Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f0f2f8569 | |||
| 5104ad3d0b | |||
| a6dbda1ea8 | |||
| 1edb8014bc | |||
| 845195b7ed | |||
| 2c9fcd7c14 | |||
| 4f9b63aa09 | |||
| a8e92971d0 | |||
| b54e477602 | |||
| 959fb08a89 | |||
| 6ac012aa3c | |||
| 3e93b31f75 | |||
| 08df208149 | |||
| 936ed73a45 | |||
| 8fee536260 | |||
| 01b4792974 | |||
| ad80bb3735 | |||
| f5f269bfd5 | |||
| 5b16769057 | |||
| 009bd410af | |||
| bcfcd7a134 | |||
| bf1276f767 | |||
| 9fa7a18351 | |||
| 50e8ee4549 | |||
| 3b5a1a8b9a | |||
| f661d9429f | |||
| ac4b109872 | |||
| b2c6e51a43 | |||
| ee2336a24b | |||
| 1e85b1cb5f | |||
| 5c6d69a69c | |||
| 299808aaaf | |||
| 7071adc774 | |||
| 8d678eed19 | |||
| ab7abd9686 | |||
| a58ce74ed0 | |||
| 2c8c2cfa14 | |||
| 0ceea1b792 | |||
| 07f2bfff6a | |||
| b9ee14f6d7 | |||
| 393fc10a69 | |||
| 1c937e58f7 | |||
| 70bbdfe760 | |||
| efe7b17d21 | |||
| 166e80bb96 | |||
| 6ac537210b | |||
| 596a39c0f5 | |||
| d118bcc717 | |||
| 48be4032a2 | |||
| 2d545c87cd | |||
| 0b48ba54f8 | |||
| c3d87289bb | |||
| 0d96627ffb | |||
| 61acc7494a | |||
| 96d5288667 | |||
| b8cd3c59f1 | |||
| 91632b52bb | |||
| d39c0e3745 | |||
| 180eaea195 | |||
| 69ed2d7117 | |||
| c420f3c909 | |||
| 0a2dea05a9 | |||
| 293c89d392 | |||
| bb5ef0c8b2 | |||
| 8f44cdc284 | |||
| 53028ed50a | |||
| 17e152c601 | |||
| 2ba6aabd4a | |||
| 98207c904d | |||
| d2b825a8ae | |||
| 913b8b37fe | |||
| f494c5ba31 | |||
| b9503685f0 | |||
| 0f4143c0e0 | |||
| 8839ed1f95 | |||
| 41b4038ad5 | |||
| 88d21eb23a | |||
| e0be7e4d4f | |||
| 0c189265e8 | |||
| b1f8cdf385 | |||
| 428a60870a | |||
| af2d6579c5 | |||
| 0f3da73ef1 | |||
| fecc9416ab | |||
| a7915db4c3 | |||
| 3a4e841c73 | |||
| bcf6ff1c08 | |||
| ad7a104ab4 | |||
| 639359f393 | |||
| 91e310b7e3 | |||
| 5b30ad01ce | |||
| 8f4f9338a9 | |||
| 67bbce3c36 | |||
| 180c3d9ae1 | |||
| 7a5883d404 | |||
| c90acefacb | |||
| d6799e08d8 | |||
| c44dccc34b | |||
| 7f1695c457 | |||
| e500d9ee26 | |||
| d5ce578929 | |||
| 3f8fe3a368 | |||
| 6b4c6fc062 | |||
| e239d6f633 | |||
| 0bbe2befce | |||
| fa4e048169 | |||
| 4f9f21f7f2 | |||
| 480176ba2d | |||
| 1190c0f6d2 | |||
| 720a284db5 | |||
| 83fdffbcef | |||
| 0ee7a49d31 | |||
| 02db5994fe | |||
| 1d7a84e8ae | |||
| a0e3dc7040 | |||
| 43495d70c7 | |||
| fa586b493c | |||
| 30cdc1430e | |||
| 27673a23e9 | |||
| 95fc299733 | |||
| 4e702eedc9 | |||
| 8ff83f2f1a | |||
| 4b8f0ad112 | |||
| dbc8bbeb6a | |||
| b3e856f40d | |||
| 8e1937fe75 | |||
| 073514fc21 | |||
| dab222d95f | |||
| a827894274 | |||
| 9a8d813d14 | |||
| e391fa94f9 | |||
| 509185dfbe | |||
| 4b1b7a8561 | |||
| b330ccca5d | |||
| 05f1237a6d | |||
| 1c270b7e39 | |||
| 1b39e9d03c | |||
| 655a2e5404 | |||
| 9c80eea045 | |||
| 175ad1dd0b | |||
| 728e97700f | |||
| 94a0fbf953 | |||
| b056e7b2b9 | |||
| 6e927f20f9 | |||
| 301f163a44 | |||
| 474652ea51 | |||
| 1bc731e782 | |||
| 292b85b5ef | |||
| dce98d50da | |||
| 10a14a8c50 | |||
| bcb5965617 | |||
| f772b3d39f | |||
| 27db8c0afe | |||
| 0e5c77d2be | |||
| 0280727985 | |||
| b4abebce00 | |||
| d89afe58d4 | |||
| c855546b9f | |||
| 7d6ee4c166 | |||
| 2fbd9d8f8c | |||
| 18e101a04e | |||
| e594421322 | |||
| 0d4bfda2fb | |||
| b83c026cdc | |||
| 46caf31075 | |||
| 2b9d696b49 | |||
| 70afff6e3b | |||
| 01bbda13e5 | |||
| c5a8f6635f | |||
| 6f441c55dc | |||
| 7b34d6cba5 | |||
| 70f9c6482f | |||
| aed6149c8c | |||
| 0834a184b8 | |||
| f61295f76c | |||
| 0920a40751 | |||
| e0b5bd75f8 | |||
| 96e188d7c4 | |||
| 1336c91865 | |||
| 9568e46f05 | |||
| 3187129672 | |||
| 44bd354eae | |||
| 94caf8b8db | |||
| 3ff1966cab | |||
| a3842155c5 | |||
| 02b3b4f80c | |||
| 0882290595 | |||
| b8d9f4ba92 | |||
| 1462c5caa5 | |||
| 58c08c855f | |||
| 9a4763ceee | |||
| f8200a6386 | |||
| e35320f8ee | |||
| 5b52e5e331 | |||
| 6ebd9f1037 | |||
| 99ab16d7be | |||
| e675b35cda | |||
| ddd7acec49 | |||
| e7aa08564b | |||
| 93ccdb8024 | |||
| 501645f918 | |||
| 4fd9d30c98 | |||
| e2b9c16959 | |||
| 4ba85c40cc | |||
| cca95f613b | |||
| 5b50a66c6c | |||
| dd508280f0 | |||
| 2927d0ca15 | |||
| d35e2400b2 | |||
| 47ed599db7 | |||
| f899f4cbe4 | |||
| b3231d2bed | |||
| c24c327151 | |||
| 5e17d7ba80 |
+346
-80
@@ -1,71 +1,79 @@
|
|||||||
version: 2.0
|
version: 2.1
|
||||||
workflows:
|
|
||||||
version: 2
|
commands:
|
||||||
build:
|
setup-home-local-bin:
|
||||||
jobs:
|
|
||||||
- build-1.11
|
|
||||||
- build-1.12
|
|
||||||
- build-1.13
|
|
||||||
- build-latest
|
|
||||||
jobs:
|
|
||||||
# build-latest serves as the template
|
|
||||||
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
|
||||||
build-latest: &build-latest
|
|
||||||
description: Builds zrepl
|
|
||||||
parameters:
|
|
||||||
image:
|
|
||||||
description: "the docker image that the job should use"
|
|
||||||
type: string
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:latest
|
|
||||||
environment:
|
|
||||||
# required by lazy.sh
|
|
||||||
TERM: xterm
|
|
||||||
working_directory: /go/src/github.com/zrepl/zrepl
|
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- run:
|
||||||
name: Setup environment variables
|
shell: /bin/bash -euo pipefail
|
||||||
command: |
|
command: |
|
||||||
# used by pip (for docs)
|
mkdir -p "$HOME/.local/bin"
|
||||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
line='export PATH="$HOME/.local/bin:$PATH"'
|
||||||
# we use modules
|
if grep "$line" $BASH_ENV >/dev/null; then
|
||||||
echo 'export GO111MODULE=on' >> $BASH_ENV
|
echo "$line" >> $BASH_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
invoke-lazy-sh:
|
||||||
|
parameters:
|
||||||
|
subcommand:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
environment:
|
||||||
|
TERM: xterm
|
||||||
|
command: ./lazy.sh <<parameters.subcommand>>
|
||||||
|
|
||||||
|
apt-update-and-install-common-deps:
|
||||||
|
steps:
|
||||||
|
- run: sudo apt update && sudo apt install gawk make
|
||||||
|
|
||||||
|
restore-cache-gomod:
|
||||||
|
steps:
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
- source
|
save-cache-gomod:
|
||||||
- protobuf
|
steps:
|
||||||
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: source
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
paths:
|
paths:
|
||||||
- ".git"
|
- "/go/pkg/mod"
|
||||||
|
|
||||||
# install deps
|
install-godep:
|
||||||
- run: wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
steps:
|
||||||
- run: echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
- apt-update-and-install-common-deps
|
||||||
- run: sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
- invoke-lazy-sh:
|
||||||
|
subcommand: godep
|
||||||
|
|
||||||
|
install-docdep:
|
||||||
|
steps:
|
||||||
|
- apt-update-and-install-common-deps
|
||||||
|
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
||||||
|
- invoke-lazy-sh:
|
||||||
|
subcommand: docdep
|
||||||
|
|
||||||
|
download-and-install-minio-client:
|
||||||
|
steps:
|
||||||
|
- setup-home-local-bin
|
||||||
|
- restore_cache:
|
||||||
|
key: minio-client-v2
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
command: |
|
||||||
|
if which mc; then exit 0; fi
|
||||||
|
sudo curl -sSL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2020-08-20T00-23-01Z \
|
||||||
|
-o "$HOME/.local/bin/mc"
|
||||||
|
sudo chmod +x "$HOME/.local/bin/mc"
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: protobuf
|
key: minio-client-v2
|
||||||
paths:
|
paths:
|
||||||
- "/usr/include/google/protobuf"
|
- "$HOME/.local/bin/mc"
|
||||||
|
|
||||||
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev gawk
|
|
||||||
- run: ./lazy.sh devsetup
|
|
||||||
|
|
||||||
- run: make zrepl-bin
|
|
||||||
- run: make vet
|
|
||||||
- run: make test
|
|
||||||
- run: make lint
|
|
||||||
- run: make release
|
|
||||||
|
|
||||||
|
|
||||||
- store_artifacts:
|
|
||||||
path: ./artifacts/release
|
|
||||||
when: always
|
|
||||||
|
|
||||||
|
upload-minio:
|
||||||
|
parameters:
|
||||||
|
src:
|
||||||
|
type: string
|
||||||
|
dst:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
- run:
|
- run:
|
||||||
shell: /bin/bash -eo pipefail
|
shell: /bin/bash -eo pipefail
|
||||||
when: always
|
when: always
|
||||||
@@ -75,36 +83,294 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
set -u # from now on
|
set -u # from now on
|
||||||
|
|
||||||
# Download and install minio
|
|
||||||
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
|
|
||||||
chmod +x ${GOPATH}/bin/mc
|
|
||||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
|
||||||
|
|
||||||
# Upload artifacts
|
|
||||||
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
|
|
||||||
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
|
|
||||||
|
|
||||||
|
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||||
|
|
||||||
|
# keep in sync with set-github-minio-status
|
||||||
|
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
mkdir -p ./artifacts
|
||||||
|
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
|
||||||
|
|
||||||
|
set-github-minio-status:
|
||||||
|
parameters:
|
||||||
|
context:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
minio-dst:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
command: |
|
||||||
|
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||||
|
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
set -u # from now on
|
||||||
|
|
||||||
|
# keep in sync with with upload-minio command
|
||||||
|
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||||
# Push Artifact Link to GitHub
|
# Push Artifact Link to GitHub
|
||||||
REPO="zrepl/zrepl"
|
REPO="zrepl/zrepl"
|
||||||
COMMIT="${CIRCLE_SHA1}"
|
COMMIT="${CIRCLE_SHA1}"
|
||||||
JOB_NAME="${CIRCLE_JOB}"
|
JOB_NAME="${CIRCLE_JOB}"
|
||||||
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT?access_token=$GITHUB_COMMIT_STATUS_TOKEN" \
|
CONTEXT="<<parameters.context>>"
|
||||||
|
DESCRIPTION="<<parameters.description>>"
|
||||||
|
TARGETURL=https://minio.cschwarz.com/minio/"$jobprefix"/"<<parameters.minio-dst>>"
|
||||||
|
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
|
||||||
-X POST \
|
-X POST \
|
||||||
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
-d '{"context":"'"$CONTEXT"'", "state": "success", "description":"'"$DESCRIPTION"'", "target_url":"'"$TARGETURL"'"}'
|
||||||
|
|
||||||
build-1.11:
|
|
||||||
<<: *build-latest
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.11
|
|
||||||
|
|
||||||
build-1.12:
|
trigger-pipeline:
|
||||||
<<: *build-latest
|
parameters:
|
||||||
docker:
|
body_no_shell_subst:
|
||||||
- image: circleci/golang:1.12
|
type: string
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
curl -X POST https://circleci.com/api/v2/project/github/zrepl/zrepl/pipeline \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H "Circle-Token: $ZREPL_BOT_CIRCLE_TOKEN" \
|
||||||
|
--data '<<parameters.body_no_shell_subst>>'
|
||||||
|
|
||||||
build-1.13:
|
parameters:
|
||||||
<<: *build-latest
|
do_ci:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
do_release:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
release_docker_baseimage_tag:
|
||||||
|
type: string
|
||||||
|
default: "1.17"
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
ci:
|
||||||
|
when: << pipeline.parameters.do_ci >>
|
||||||
|
jobs:
|
||||||
|
- quickcheck-docs
|
||||||
|
- quickcheck-go: &quickcheck-go-smoketest
|
||||||
|
name: quickcheck-go-amd64-linux-1.17
|
||||||
|
goversion: &latest-go-release "1.17"
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- test-go-on-latest-go-release:
|
||||||
|
goversion: *latest-go-release
|
||||||
|
- quickcheck-go:
|
||||||
|
requires:
|
||||||
|
- quickcheck-go-amd64-linux-1.17 #quickcheck-go-smoketest.name
|
||||||
|
matrix: &quickcheck-go-matrix
|
||||||
|
alias: quickcheck-go-matrix
|
||||||
|
parameters:
|
||||||
|
goversion: [*latest-go-release, "1.12"]
|
||||||
|
goos: ["linux", "freebsd"]
|
||||||
|
goarch: ["amd64", "arm64"]
|
||||||
|
exclude:
|
||||||
|
# don't re-do quickcheck-go-smoketest
|
||||||
|
- goversion: *latest-go-release
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
# not supported by Go 1.12
|
||||||
|
- goversion: "1.12"
|
||||||
|
goos: freebsd
|
||||||
|
goarch: arm64
|
||||||
|
|
||||||
|
release:
|
||||||
|
when: << pipeline.parameters.do_release >>
|
||||||
|
jobs:
|
||||||
|
- release-build
|
||||||
|
- release-deb:
|
||||||
|
requires:
|
||||||
|
- release-build
|
||||||
|
- release-rpm:
|
||||||
|
requires:
|
||||||
|
- release-build
|
||||||
|
- release-upload:
|
||||||
|
requires:
|
||||||
|
- release-build
|
||||||
|
- release-deb
|
||||||
|
- release-rpm
|
||||||
|
|
||||||
|
periodic:
|
||||||
|
triggers:
|
||||||
|
- schedule:
|
||||||
|
cron: "00 17 * * *"
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- stable
|
||||||
|
- problame/circleci-build
|
||||||
|
jobs:
|
||||||
|
- periodic-full-pipeline-run
|
||||||
|
|
||||||
|
zrepl.github.io:
|
||||||
|
jobs:
|
||||||
|
- publish-zrepl-github-io:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- stable
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quickcheck-docs:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.13
|
- image: cimg/base:2020.08
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-docdep
|
||||||
|
- run: make docs
|
||||||
|
|
||||||
|
- download-and-install-minio-client
|
||||||
|
- upload-minio:
|
||||||
|
src: artifacts
|
||||||
|
dst: ""
|
||||||
|
- set-github-minio-status:
|
||||||
|
context: artifacts/${CIRCLE_JOB}
|
||||||
|
description: artifacts of CI job ${CIRCLE_JOB}
|
||||||
|
minio-dst: ""
|
||||||
|
|
||||||
|
quickcheck-go:
|
||||||
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
|
goos:
|
||||||
|
type: string
|
||||||
|
goarch:
|
||||||
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:<<parameters.goversion>>
|
||||||
|
environment:
|
||||||
|
GOOS: <<parameters.goos>>
|
||||||
|
GOARCH: <<parameters.goarch>>
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- restore-cache-gomod
|
||||||
|
- run: go mod download
|
||||||
|
- run: cd build && go mod download
|
||||||
|
- save-cache-gomod
|
||||||
|
|
||||||
|
- install-godep
|
||||||
|
- run: make formatcheck
|
||||||
|
- run: make generate-platform-test-list
|
||||||
|
- run: make zrepl-bin test-platform-bin
|
||||||
|
- run: make vet
|
||||||
|
- run: make lint
|
||||||
|
|
||||||
|
- download-and-install-minio-client
|
||||||
|
- run: rm -f artifacts/generate-platform-test-list
|
||||||
|
- store_artifacts:
|
||||||
|
path: artifacts
|
||||||
|
- upload-minio:
|
||||||
|
src: artifacts
|
||||||
|
dst: ""
|
||||||
|
- set-github-minio-status:
|
||||||
|
context: artifacts/${CIRCLE_JOB}
|
||||||
|
description: artifacts of CI job ${CIRCLE_JOB}
|
||||||
|
minio-dst: ""
|
||||||
|
|
||||||
|
test-go-on-latest-go-release:
|
||||||
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:<<parameters.goversion>>
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- restore-cache-gomod
|
||||||
|
- run: make test-go
|
||||||
|
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
|
||||||
|
|
||||||
|
release-build:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths: [.]
|
||||||
|
release-deb:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: make debs-docker
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- "artifacts/*.deb"
|
||||||
|
|
||||||
|
release-rpm:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: make rpms-docker
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- "artifacts/*.rpm"
|
||||||
|
|
||||||
|
release-upload:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:2020.08
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- store_artifacts:
|
||||||
|
path: artifacts
|
||||||
|
- download-and-install-minio-client
|
||||||
|
- upload-minio:
|
||||||
|
src: artifacts
|
||||||
|
dst: ""
|
||||||
|
- set-github-minio-status:
|
||||||
|
context: artifacts/release
|
||||||
|
description: CI-generated release artifacts
|
||||||
|
minio-dst: ""
|
||||||
|
|
||||||
|
periodic-full-pipeline-run:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:2020.08
|
||||||
|
steps:
|
||||||
|
- trigger-pipeline:
|
||||||
|
body_no_shell_subst: '{"branch":"<<pipeline.git.branch>>", "parameters": { "do_ci": true, "do_release": true }}'
|
||||||
|
|
||||||
|
publish-zrepl-github-io:
|
||||||
|
docker:
|
||||||
|
- image: cimg/python:3.7
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- invoke-lazy-sh:
|
||||||
|
subcommand: docdep
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
git config --global user.email "zreplbot@cschwarz.com"
|
||||||
|
git config --global user.name "zrepl-github-io-ci"
|
||||||
|
|
||||||
|
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
||||||
|
- run: ssh-add -D
|
||||||
|
# the default circleci ssh config only additional ssh keys for Host !github.com
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
cat > ~/.ssh/config \<<EOF
|
||||||
|
Host *
|
||||||
|
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
|
||||||
|
EOF
|
||||||
|
- add_ssh_keys:
|
||||||
|
fingerprints:
|
||||||
|
# deploy key for zrepl.github.io
|
||||||
|
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
|
||||||
|
|
||||||
|
- run: bash -x docs/publish.sh -c -a
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
COMMIT="$1"
|
||||||
|
GO_VERSION="$2"
|
||||||
|
|
||||||
|
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
||||||
|
-H 'Accept: application/vnd.github.v3+json' \
|
||||||
|
-H "Authorization: token $GITHUB_ACCESS_TOKEN" \
|
||||||
|
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$COMMIT"'", "go_version": "'"$GO_VERSION"'" }}'
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
github: problame
|
||||||
patreon: zrepl
|
patreon: zrepl
|
||||||
liberapay: zrepl
|
liberapay: zrepl
|
||||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
|
|||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
dist: xenial
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
env: # for allow_failures: https://docs.travis-ci.com/user/customizing-the-build/
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
|
|
||||||
- language: go
|
|
||||||
name: "Build in Docker (docs/installation.rst)"
|
|
||||||
script:
|
|
||||||
- sudo docker build -t zrepl_build -f build.Dockerfile .
|
|
||||||
- |
|
|
||||||
sudo docker run -it --rm \
|
|
||||||
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
|
|
||||||
--user "$(id -u):$(id -g)" \
|
|
||||||
zrepl_build make vendordeps release
|
|
||||||
|
|
||||||
- &zrepl_build_template
|
|
||||||
language: go
|
|
||||||
go_import_path: github.com/zrepl/zrepl
|
|
||||||
before_install:
|
|
||||||
- wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
|
||||||
- echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
|
||||||
- sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
|
||||||
- ./lazy.sh godep
|
|
||||||
- make vendordeps
|
|
||||||
script:
|
|
||||||
- make
|
|
||||||
- make vet
|
|
||||||
- make test
|
|
||||||
- make lint
|
|
||||||
- make artifacts/zrepl-freebsd-amd64
|
|
||||||
- make artifacts/zrepl-linux-amd64
|
|
||||||
- make artifacts/zrepl-darwin-amd64
|
|
||||||
go:
|
|
||||||
- "1.11"
|
|
||||||
|
|
||||||
- <<: *zrepl_build_template
|
|
||||||
go:
|
|
||||||
- "1.12"
|
|
||||||
|
|
||||||
- <<: *zrepl_build_template
|
|
||||||
go:
|
|
||||||
- "master"
|
|
||||||
|
|
||||||
- &zrepl_docs_template
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.4"
|
|
||||||
install:
|
|
||||||
- sudo apt-get install libgirepository1.0-dev
|
|
||||||
- pip install -r docs/requirements.txt
|
|
||||||
script:
|
|
||||||
- make docs
|
|
||||||
- <<: *zrepl_docs_template
|
|
||||||
python:
|
|
||||||
- "3.5"
|
|
||||||
- <<: *zrepl_docs_template
|
|
||||||
python:
|
|
||||||
- "3.6"
|
|
||||||
- <<: *zrepl_docs_template
|
|
||||||
python:
|
|
||||||
- "3.7"
|
|
||||||
|
|
||||||
|
|
||||||
allow_failures:
|
|
||||||
- <<: *zrepl_build_template
|
|
||||||
go:
|
|
||||||
- "master"
|
|
||||||
@@ -17,19 +17,31 @@ endif
|
|||||||
GO := go
|
GO := go
|
||||||
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||||
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
|
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
|
||||||
|
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
|
||||||
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
|
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
|
||||||
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
||||||
GO_ENV_VARS := GO111MODULE=on
|
GO_ENV_VARS := GO111MODULE=on
|
||||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||||
GO_MOD_READONLY := -mod=readonly
|
GO_MOD_READONLY := -mod=readonly
|
||||||
GO_BUILDFLAGS := $(GO_MOD_READONLY)
|
GO_EXTRA_BUILDFLAGS :=
|
||||||
|
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
||||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
|
GOCOVMERGE := gocovmerge
|
||||||
|
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.17
|
||||||
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||||
|
|
||||||
|
ifneq ($(GOARM),)
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
|
else
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
|
||||||
|
endif
|
||||||
|
|
||||||
.PHONY: printvars
|
.PHONY: printvars
|
||||||
printvars:
|
printvars:
|
||||||
@echo GOOS=$(GOOS)
|
@echo GOOS=$(GOOS)
|
||||||
@echo GOARCH=$(GOARCH)
|
@echo GOARCH=$(GOARCH)
|
||||||
|
@echo GOARM=$(GOARM)
|
||||||
|
|
||||||
|
|
||||||
##################### PRODUCING A RELEASE #############
|
##################### PRODUCING A RELEASE #############
|
||||||
@@ -37,7 +49,7 @@ printvars:
|
|||||||
|
|
||||||
release: clean
|
release: clean
|
||||||
# no cross-platform support for target test
|
# no cross-platform support for target test
|
||||||
$(MAKE) test
|
$(MAKE) test-go
|
||||||
$(MAKE) bins-all
|
$(MAKE) bins-all
|
||||||
$(MAKE) noarch
|
$(MAKE) noarch
|
||||||
$(MAKE) wrapup-and-checksum
|
$(MAKE) wrapup-and-checksum
|
||||||
@@ -47,6 +59,85 @@ ifeq (SIGN, 1)
|
|||||||
endif
|
endif
|
||||||
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||||
|
|
||||||
|
release-docker: $(ARTIFACTDIR)
|
||||||
|
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile
|
||||||
|
docker build -t zrepl_release --pull -f artifacts/release-docker.Dockerfile .
|
||||||
|
docker run --rm -i -v $(CURDIR):/src -u $$(id -u):$$(id -g) \
|
||||||
|
zrepl_release \
|
||||||
|
make release GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||||
|
|
||||||
|
debs-docker:
|
||||||
|
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=deb
|
||||||
|
rpms-docker:
|
||||||
|
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=rpm
|
||||||
|
_debs_or_rpms_docker: # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=amd64
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm64
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm GOARM=7
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=386
|
||||||
|
|
||||||
|
rpm: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
||||||
|
$(eval _ZREPL_RPM_VERSION := $(subst -,.,$(_ZREPL_VERSION)))
|
||||||
|
$(eval _ZREPL_RPM_TOPDIR_ABS := $(CURDIR)/$(ARTIFACTDIR)/rpmbuild)
|
||||||
|
rm -rf "$(_ZREPL_RPM_TOPDIR_ABS)"
|
||||||
|
mkdir "$(_ZREPL_RPM_TOPDIR_ABS)"
|
||||||
|
mkdir -p "$(_ZREPL_RPM_TOPDIR_ABS)"/{SPECS,RPMS,BUILD,BUILDROOT}
|
||||||
|
sed "s/^Version:.*/Version: $(_ZREPL_RPM_VERSION)/g" \
|
||||||
|
packaging/rpm/zrepl.spec > $(_ZREPL_RPM_TOPDIR_ABS)/SPECS/zrepl.spec
|
||||||
|
|
||||||
|
# see /usr/lib/rpm/platform
|
||||||
|
ifeq ($(GOARCH),amd64)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := x86_64)
|
||||||
|
else ifeq ($(GOARCH), 386)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := i386)
|
||||||
|
else ifeq ($(GOARCH), arm64)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := aarch64)
|
||||||
|
else ifeq ($(GOARCH), arm)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := armv7hl)
|
||||||
|
else
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := $(GOARCH))
|
||||||
|
endif
|
||||||
|
rpmbuild \
|
||||||
|
--build-in-place \
|
||||||
|
--define "_sourcedir $(CURDIR)" \
|
||||||
|
--define "_topdir $(_ZREPL_RPM_TOPDIR_ABS)" \
|
||||||
|
--define "_zrepl_binary_filename zrepl-$(ZREPL_TARGET_TUPLE)" \
|
||||||
|
--target $(_ZREPL_RPMBUILD_TARGET) \
|
||||||
|
-bb "$(_ZREPL_RPM_TOPDIR_ABS)"/SPECS/zrepl.spec
|
||||||
|
cp "$(_ZREPL_RPM_TOPDIR_ABS)"/RPMS/$(_ZREPL_RPMBUILD_TARGET)/zrepl-$(_ZREPL_RPM_VERSION)*.rpm $(ARTIFACTDIR)/
|
||||||
|
|
||||||
|
rpm-docker:
|
||||||
|
docker build -t zrepl_rpm_pkg --pull -f packaging/rpm/Dockerfile .
|
||||||
|
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
||||||
|
zrepl_rpm_pkg \
|
||||||
|
make rpm GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||||
|
|
||||||
|
deb: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
||||||
|
|
||||||
|
cp packaging/deb/debian/changelog.template packaging/deb/debian/changelog
|
||||||
|
sed -i 's/DATE_DASH_R_OUTPUT/$(shell date -R)/' packaging/deb/debian/changelog
|
||||||
|
VERSION="$(subst -,.,$(_ZREPL_VERSION))"; \
|
||||||
|
export VERSION="$${VERSION#v}"; \
|
||||||
|
sed -i 's/VERSION/'"$$VERSION"'/' packaging/deb/debian/changelog
|
||||||
|
|
||||||
|
ifeq ($(GOARCH), arm)
|
||||||
|
$(eval DEB_HOST_ARCH := armhf)
|
||||||
|
else ifeq ($(GOARCH), 386)
|
||||||
|
$(eval DEB_HOST_ARCH := i386)
|
||||||
|
else
|
||||||
|
$(eval DEB_HOST_ARCH := $(GOARCH))
|
||||||
|
endif
|
||||||
|
|
||||||
|
export ZREPL_DPKG_ZREPL_BINARY_FILENAME=zrepl-$(ZREPL_TARGET_TUPLE); \
|
||||||
|
dpkg-buildpackage -b --no-sign --host-arch $(DEB_HOST_ARCH)
|
||||||
|
cp ../*.deb artifacts/
|
||||||
|
|
||||||
|
deb-docker:
|
||||||
|
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
||||||
|
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
||||||
|
zrepl_debian_pkg \
|
||||||
|
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||||
|
|
||||||
# expects `release` target to have run before
|
# expects `release` target to have run before
|
||||||
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
wrapup-and-checksum:
|
wrapup-and-checksum:
|
||||||
@@ -58,6 +149,7 @@ wrapup-and-checksum:
|
|||||||
-acf $(NOARCH_TARBALL) \
|
-acf $(NOARCH_TARBALL) \
|
||||||
$(ARTIFACTDIR)/docs/html \
|
$(ARTIFACTDIR)/docs/html \
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||||
$(ARTIFACTDIR)/go_env.txt \
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
dist \
|
dist \
|
||||||
config/samples
|
config/samples
|
||||||
@@ -90,15 +182,16 @@ clean: docs-clean
|
|||||||
rm -rf "$(ARTIFACTDIR)"
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
|
||||||
##################### BINARIES #####################
|
##################### BINARIES #####################
|
||||||
.PHONY: bins-all lint test vet zrepl-bin platformtest-bin
|
.PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
|
||||||
|
|
||||||
BINS_ALL_TARGETS := zrepl-bin platformtest-bin vet lint
|
BINS_ALL_TARGETS := zrepl-bin test-platform-bin vet lint
|
||||||
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
|
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
|
||||||
bins-all:
|
bins-all:
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=386
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=386
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=darwin GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=darwin GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
|
||||||
@@ -112,35 +205,104 @@ endif
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
||||||
test:
|
|
||||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) ./...
|
|
||||||
vet:
|
vet:
|
||||||
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||||
|
|
||||||
|
test-go: $(ARTIFACTDIR)
|
||||||
|
rm -f "$(ARTIFACTDIR)/gotest.cover"
|
||||||
|
ifeq ($(COVER),1)
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
-coverpkg github.com/zrepl/zrepl/... \
|
||||||
|
-covermode atomic \
|
||||||
|
-coverprofile "$(ARTIFACTDIR)/gotest.cover" \
|
||||||
|
./...
|
||||||
|
else
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
./...
|
||||||
|
endif
|
||||||
|
|
||||||
zrepl-bin:
|
zrepl-bin:
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(GOOS)-$(GOARCH)"
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||||
|
|
||||||
platformtest-bin:
|
generate-platform-test-list:
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(GOOS)-$(GOARCH)" ./platformtest/harness
|
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
||||||
|
|
||||||
##################### DEV TARGETS #####################
|
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
||||||
# not part of the build, must do that manually
|
cover-platform-bin:
|
||||||
.PHONY: generate format platformtest
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
-c -o "$(COVER_PLATFORM_BIN_PATH)" \
|
||||||
|
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
||||||
|
./platformtest/harness
|
||||||
|
cover-platform:
|
||||||
|
# do not track dependency on cover-platform-bin to allow build of binary outside of test VM
|
||||||
|
export _TEST_PLATFORM_CMD="$(COVER_PLATFORM_BIN_PATH) \
|
||||||
|
-test.coverprofile \"$(ARTIFACTDIR)/platformtest.cover\" \
|
||||||
|
-test.v \
|
||||||
|
__DEVEL--i-heard-you-like-tests"; \
|
||||||
|
$(MAKE) _test-or-cover-platform-impl
|
||||||
|
|
||||||
generate:
|
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
|
||||||
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
test-platform-bin:
|
||||||
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
$(GO_BUILD) -o "$(TEST_PLATFORM_BIN_PATH)" ./platformtest/harness
|
||||||
|
test-platform:
|
||||||
format:
|
export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \
|
||||||
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
|
$(MAKE) _test-or-cover-platform-impl
|
||||||
|
|
||||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||||
platformtest: # do not track dependency on platformtest-bin to allow build of platformtest outside of test VM
|
ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
||||||
"$(ARTIFACTDIR)/platformtest-$(GOOS)-$(GOARCH)" -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)"
|
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||||
|
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||||
|
ZREPL_PLATFORMTEST_ARGS :=
|
||||||
|
_test-or-cover-platform-impl: $(ARTIFACTDIR)
|
||||||
|
ifndef _TEST_PLATFORM_CMD
|
||||||
|
$(error _TEST_PLATFORM_CMD is undefined, caller 'cover-platform' or 'test-platform' should have defined it)
|
||||||
|
endif
|
||||||
|
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||||
|
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
||||||
|
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||||
|
$(_TEST_PLATFORM_CMD) \
|
||||||
|
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||||
|
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||||
|
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||||
|
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
||||||
|
$(ZREPL_PLATFORMTEST_ARGS)
|
||||||
|
|
||||||
|
cover-merge: $(ARTIFACTDIR)
|
||||||
|
$(GOCOVMERGE) $(ARTIFACTDIR)/platformtest.cover $(ARTIFACTDIR)/gotest.cover > $(ARTIFACTDIR)/merged.cover
|
||||||
|
cover-html: cover-merge
|
||||||
|
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
||||||
|
|
||||||
|
cover-full:
|
||||||
|
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
||||||
|
$(MAKE) test-go COVER=1
|
||||||
|
$(MAKE) cover-platform-bin
|
||||||
|
$(MAKE) cover-platform
|
||||||
|
$(MAKE) cover-html
|
||||||
|
|
||||||
|
##################### DEV TARGETS #####################
|
||||||
|
# not part of the build, must do that manually
|
||||||
|
.PHONY: generate formatcheck format
|
||||||
|
|
||||||
|
generate: generate-platform-test-list
|
||||||
|
protoc -I=replication/logic/pdu --go_out=replication/logic/pdu --go-grpc_out=replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||||
|
protoc -I=rpc/grpcclientidentity/example --go_out=rpc/grpcclientidentity/example/pdu --go-grpc_out=rpc/grpcclientidentity/example/pdu rpc/grpcclientidentity/example/grpcauth.proto
|
||||||
|
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
||||||
|
|
||||||
|
GOIMPORTS := goimports -srcdir . -local 'github.com/zrepl/zrepl'
|
||||||
|
FINDSRCFILES := find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go'
|
||||||
|
|
||||||
|
formatcheck:
|
||||||
|
@# goimports doesn't have a knob to exit with non-zero status code if formatting is needed
|
||||||
|
@# see https://go-review.googlesource.com/c/tools/+/237378
|
||||||
|
@ affectedfiles=$$($(GOIMPORTS) -l $(shell $(FINDSRCFILES)) | tee /dev/stderr | wc -l); test "$$affectedfiles" = 0
|
||||||
|
|
||||||
|
format:
|
||||||
|
@ $(GOIMPORTS) -w -d $(shell $(FINDSRCFILES))
|
||||||
|
|
||||||
##################### NOARCH #####################
|
##################### NOARCH #####################
|
||||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
|
|
||||||
|
|
||||||
$(ARTIFACTDIR):
|
$(ARTIFACTDIR):
|
||||||
@@ -148,20 +310,27 @@ $(ARTIFACTDIR):
|
|||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
mkdir -p "$@"
|
mkdir -p "$@"
|
||||||
|
|
||||||
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs
|
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion:
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) bashcomp "$@"
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion:
|
||||||
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
|
||||||
|
|
||||||
$(ARTIFACTDIR)/go_env.txt:
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
$(GO_ENV_VARS) $(GO) env > $@
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
|
$(GO) version >> $@
|
||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
docs: $(ARTIFACTDIR)/docs
|
||||||
|
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
|
||||||
make -C docs \
|
make -C docs \
|
||||||
html \
|
html \
|
||||||
BUILDDIR=../artifacts/docs \
|
BUILDDIR=../artifacts/docs \
|
||||||
|
SPHINXOPTS="-W --keep-going -n"
|
||||||
|
|
||||||
docs-clean:
|
docs-clean:
|
||||||
make -C docs \
|
make -C docs \
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://zrepl.github.io)
|
[](https://zrepl.github.io)
|
||||||
|
[](https://patreon.com/zrepl)
|
||||||
|
[](https://github.com/sponsors/problame)
|
||||||
|
[](https://liberapay.com/zrepl/donate)
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
[](https://liberapay.com/zrepl/donate)
|
|
||||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
@@ -19,92 +21,92 @@ zrepl is a one-stop ZFS backup & replication solution.
|
|||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
1. Does you feature request require default values / some kind of configuration?
|
1. Does your feature request require default values / some kind of configuration?
|
||||||
If so, think of an expressive configuration example.
|
If so, think of an expressive configuration example.
|
||||||
2. Think of at least one use case that generalizes from your concrete application.
|
2. Think of at least one use case that generalizes from your concrete application.
|
||||||
3. Open an issue on GitHub with example conf & use case attached.
|
3. Open an issue on GitHub with example conf & use case attached.
|
||||||
|
4. **Optional**: [Post a bounty](https://www.bountysource.com/teams/zrepl) on the issue, or [contact Christian Schwarz](https://cschwarz.com) for contract work.
|
||||||
|
|
||||||
The above does not apply if you already implemented everything.
|
The above does not apply if you already implemented everything.
|
||||||
Check out the *Coding Workflow* section below for details.
|
Check out the *Coding Workflow* section below for details.
|
||||||
|
|
||||||
## Package Maintainer Information
|
## Building, Releasing, Downstream-Packaging
|
||||||
|
|
||||||
* Follow the steps in `docs/installation.rst -> Compiling from Source` and read the Makefile / shell scripts used in this process.
|
This section provides an overview of the zrepl build & release process.
|
||||||
* Make sure your distro is compatible with the paths in `docs/installation.rst`.
|
Check out `docs/installation/compile-from-source.rst` for build-from-source instructions.
|
||||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
|
||||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
|
||||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
|
||||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
|
||||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
|
||||||
* Use `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
|
||||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
|
||||||
|
|
||||||
## Developer Documentation
|
### Overview
|
||||||
|
|
||||||
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||||
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
||||||
|
|
||||||
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
Install **build dependencies** using `./lazy.sh devsetup`.
|
||||||
|
`lazy.sh` uses `python3-pip` to fetch the build dependencies for the docs - you might want to use a [venv](https://docs.python.org/3/library/venv.html).
|
||||||
|
If you just want to install the Go dependencies, run `./lazy.sh godep`.
|
||||||
|
|
||||||
### Overall Architecture
|
The **test suite** is split into pure **Go tests** (`make test-go`) and **platform tests** that interact with ZFS and thus generally **require root privileges** (`sudo make test-platform`).
|
||||||
|
Platform tests run on their own pool with the name `zreplplatformtest`, which is created using the file vdev in `/tmp`.
|
||||||
|
|
||||||
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
|
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
|
||||||
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
|
An HTML report can be generated using `make cover-html`.
|
||||||
|
|
||||||
### Project Structure
|
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
|
||||||
|
|
||||||
```
|
### Build & Release Process
|
||||||
├── artifacts # build artifcats generate by make
|
|
||||||
├── cli # wrapper around CLI package cobra
|
|
||||||
├── client # all subcommands that are not `daemon`
|
|
||||||
├── config # config data types (=> package yaml-config)
|
|
||||||
│ └── samples
|
|
||||||
├── daemon # the implementation of `zrepl daemon` subcommand
|
|
||||||
│ ├── filters
|
|
||||||
│ ├── job # job implementations
|
|
||||||
│ ├── logging # logging outlets + formatters
|
|
||||||
│ ├── nethelpers
|
|
||||||
│ ├── prometheus
|
|
||||||
│ ├── pruner # pruner implementation
|
|
||||||
│ ├── snapper # snapshotter implementation
|
|
||||||
├── docs # sphinx-based documentation
|
|
||||||
├── dist # supplemental material for users & package maintainers
|
|
||||||
│ ├── **/*.rst # documentation in reStructuredText
|
|
||||||
│ ├── sphinxconf
|
|
||||||
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
|
||||||
│ ├── requirements.txt # pip3 requirements to build documentation
|
|
||||||
│ ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
|
||||||
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
|
||||||
├── endpoint # implementation of replication endpoints (=> package replication)
|
|
||||||
├── logger # our own logger package
|
|
||||||
├── pruning # pruning rules (the logic, not the actual execution)
|
|
||||||
│ └── retentiongrid
|
|
||||||
├── replication
|
|
||||||
│ ├── driver # the driver of the replication logic (status reporting, error handling)
|
|
||||||
│ ├── logic # planning & executing replication steps via rpc
|
|
||||||
| | └── pdu # the generated gRPC & protobuf code used in replication (and endpoints)
|
|
||||||
│ └── report # the JSON-serializable report datastructures exposed to the client
|
|
||||||
├── rpc # the hybrid gRPC + ./dataconn RPC client: connects to a remote replication.Endpoint
|
|
||||||
│ ├── dataconn # Bulk data-transfer RPC protocol
|
|
||||||
│ ├── grpcclientidentity # adaptor to inject package transport's 'client identity' concept into gRPC contexts
|
|
||||||
│ ├── netadaptor # adaptor to convert a package transport's Connecter and Listener into net.* primitives
|
|
||||||
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
|
||||||
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
|
||||||
├── tlsconf # abstraction for Go TLS server + client config
|
|
||||||
├── transport # transports implementation
|
|
||||||
│ ├── fromconfig
|
|
||||||
│ ├── local
|
|
||||||
│ ├── ssh
|
|
||||||
│ ├── tcp
|
|
||||||
│ └── tls
|
|
||||||
├── util
|
|
||||||
├── vendor # managed by dep
|
|
||||||
├── version # abstraction for versions (filled during build by Makefile)
|
|
||||||
└── zfs # zfs(8) wrappers
|
|
||||||
```
|
|
||||||
|
|
||||||
### Coding Workflow
|
**The `Makefile` is catering to the needs of developers & CI, not distro packagers**.
|
||||||
|
It provides phony targets for
|
||||||
|
* local development (building, running tests, etc)
|
||||||
|
* building a release in Docker (used by the CI & release management)
|
||||||
|
* building .deb and .rpm packages out of the release artifacts.
|
||||||
|
|
||||||
|
**Build tooling & dependencies** are documented as code in `lazy.sh`.
|
||||||
|
Go dependencies are then fetched by the go command and pip dependencies are pinned through a `requirements.txt`.
|
||||||
|
|
||||||
|
**We use CircleCI for continuous integration**.
|
||||||
|
There are two workflows:
|
||||||
|
|
||||||
|
* `ci` runs for every commit / branch / tag pushed to GitHub.
|
||||||
|
It is supposed to run very fast (<5min and provides quick feedback to developers).
|
||||||
|
It runs formatting checks, lints and tests on the most important OSes / architectures.
|
||||||
|
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
||||||
|
|
||||||
|
* `release` runs
|
||||||
|
* on manual triggers through the CircleCI API (in order to produce a release)
|
||||||
|
* periodically on `master`
|
||||||
|
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
||||||
|
|
||||||
|
**Releases** are issued via Git tags + GitHub Releases feature.
|
||||||
|
The procedure to issue a release is as follows:
|
||||||
|
* Issue the source release:
|
||||||
|
* Git tag the release on the `master` branch.
|
||||||
|
* Push the tag.
|
||||||
|
* Run `./docs/publish.sh` to re-build & push zrepl.github.io.
|
||||||
|
* Issue the official binary release:
|
||||||
|
* Run the `release` pipeline (triggered via CircleCI API)
|
||||||
|
* Download the artifacts to the release manager's machine.
|
||||||
|
* Create a GitHub release, edit the changelog, upload all the release artifacts, including .rpm and .deb files.
|
||||||
|
* Issue the GitHub release.
|
||||||
|
* Add the .rpm and .deb files to the official zrepl repos, publish those.
|
||||||
|
|
||||||
|
**Official binary releases are not re-built when Go receives an update. If the Go update is critical to zrepl (e.g. a Go security update that affects zrepl), we'd issue a new source release**.
|
||||||
|
The rationale for this is that whereas distros provide a mechanism for this (`$zrepl_source_release-$distro_package_revision`), GitHub Releases doesn't which means we'd need to update the existing GitHub release's assets, which nobody would notice (no RSS feed updates, etc.).
|
||||||
|
Downstream packagers can read the changelog to determine whether they want to push that minor release into their distro or simply skip it.
|
||||||
|
|
||||||
|
### Additional Notes to Distro Package Maintainers
|
||||||
|
|
||||||
|
* Use `sudo make test-platform-bin && sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
|
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||||
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
|
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
|
||||||
|
This is how `zrepl version` knows what version number to show.
|
||||||
|
Your build system should set the `ldFlags` flags appropriately and add a prefix or suffix that indicates that the given zrepl binary is a distro build, not an official one.
|
||||||
|
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing Code
|
||||||
|
|
||||||
* Open an issue when starting to hack on a new feature
|
* Open an issue when starting to hack on a new feature
|
||||||
* Commits should reference the issue they are related to
|
* Commits should reference the issue they are related to
|
||||||
@@ -114,9 +116,6 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
|
|
||||||
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
||||||
|
|
||||||
* Config-breaking changes must contain a line `BREAK CONFIG` in the commit message
|
|
||||||
* Other breaking changes must contain a line `BREAK` in the commit message
|
|
||||||
|
|
||||||
### Glossary & Naming Inconsistencies
|
### Glossary & Naming Inconsistencies
|
||||||
|
|
||||||
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
||||||
@@ -133,4 +132,3 @@ variables and types are often named *dataset* when they in fact refer to a *file
|
|||||||
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
||||||
|
|
||||||
However, new contributions & patches should fix naming without further notice in the commit message.
|
However, new contributions & patches should fix naming without further notice in the commit message.
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
FROM golang:latest
|
FROM !SUBSTITUTED_BY_MAKEFILE
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
unzip
|
unzip \
|
||||||
|
gawk
|
||||||
|
|
||||||
ADD build.installprotoc.bash ./
|
ADD build.installprotoc.bash ./
|
||||||
RUN bash build.installprotoc.bash
|
RUN bash build.installprotoc.bash
|
||||||
@@ -22,7 +23,7 @@ RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
||||||
ADD go.mod go.sum ./
|
ADD build/ /tmp/build
|
||||||
RUN /tmp/lazy.sh godep
|
RUN /tmp/lazy.sh godep
|
||||||
|
|
||||||
RUN chmod -R 0777 /go
|
RUN chmod -R 0777 /go
|
||||||
|
|||||||
+5
-40
@@ -3,49 +3,14 @@ module github.com/zrepl/zrepl/build
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
|
||||||
github.com/alvaroloes/enumer v1.1.1
|
github.com/alvaroloes/enumer v1.1.1
|
||||||
github.com/fatih/color v1.7.0 // indirect
|
github.com/golangci/golangci-lint v1.35.2
|
||||||
github.com/go-critic/go-critic v0.3.4 // indirect
|
|
||||||
github.com/gogo/protobuf v1.2.1 // indirect
|
|
||||||
github.com/golang/mock v1.2.0 // indirect
|
|
||||||
github.com/golang/protobuf v1.2.0
|
|
||||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
|
||||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
|
|
||||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
|
||||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
|
|
||||||
github.com/golangci/golangci-lint v1.17.1
|
|
||||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
|
|
||||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
|
|
||||||
github.com/golangci/misspell v0.3.4 // indirect
|
github.com/golangci/misspell v0.3.4 // indirect
|
||||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||||
github.com/google/go-cmp v0.3.0 // indirect
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
|
||||||
github.com/pkg/errors v0.8.1 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.4.2 // indirect
|
|
||||||
github.com/spf13/afero v1.2.2 // indirect
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/viper v1.3.2 // indirect
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
||||||
github.com/stretchr/testify v1.4.0 // indirect
|
golang.org/x/tools v0.0.0-20210105210202-9ed45478a130
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
google.golang.org/protobuf v1.25.0
|
||||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
|
|
||||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
|
|
||||||
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
// invalid dates in transitive dependencies (first validated in Go 1.13, didn't fail in earlier Go versions)
|
|
||||||
replace (
|
|
||||||
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
|
|
||||||
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead => github.com/go-critic/go-critic v0.3.5-0.20190210220443-ee9bf5809ead
|
|
||||||
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
|
|
||||||
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0 => github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0
|
|
||||||
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
|
|
||||||
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
|
|
||||||
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
|
|
||||||
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
|
|
||||||
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
|
|
||||||
golang.org/x/tools v0.0.0-20190125232054-379209517ffe => golang.org/x/tools v0.0.0-20190205201329-379209517ffe
|
|
||||||
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34
|
|
||||||
)
|
)
|
||||||
|
|||||||
+532
-114
@@ -1,66 +1,133 @@
|
|||||||
|
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/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
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/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
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 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 h1:pFks+oaqVWDFq0KsLQZFB2pQGB5Us0HfMSBENtieXOs=
|
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 h1:XTrzB+F8+SpRmbhAH8HLxhiiG6nYNwaBZjrFps1oWEk=
|
||||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/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/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-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/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/alvaroloes/enumer v1.1.1 h1:v+1scNqEhSFAPb9tLwblQegeTXJhnw8hf9O0amTbOvQ=
|
github.com/alvaroloes/enumer v1.1.1 h1:v+1scNqEhSFAPb9tLwblQegeTXJhnw8hf9O0amTbOvQ=
|
||||||
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
|
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/ashanbrown/forbidigo v1.0.0 h1:QdNXBduDUopc3GW+YVYZn8jzmIMklQiCfdN2N5+dQeE=
|
||||||
|
github.com/ashanbrown/forbidigo v1.0.0/go.mod h1:PH+zMRWE15yW69fYfe7Kn8nYR6yYyafc3ntEGh2BBAg=
|
||||||
|
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/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/bombsimon/wsl/v3 v3.1.0 h1:E5SRssoBgtVFPcYWUOFJEcgaySgdtTNYzsSKDOY7ss8=
|
||||||
|
github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
|
||||||
|
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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
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-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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
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/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
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/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
github.com/go-critic/go-critic v0.5.3 h1:xQEweNxzBNpSqI3wotXZAixRarETng3PTG4pkcrLCOA=
|
||||||
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
|
github.com/go-critic/go-critic v0.5.3/go.mod h1:2Lrs1m4jtOnnG/EdezbSpAoL0F2pRW+9HWJUZ+QaktY=
|
||||||
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-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.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||||
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
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 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||||
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
|
|
||||||
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
|
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/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||||
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
|
||||||
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
|
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/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||||
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
|
||||||
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
|
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/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/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||||
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
|
||||||
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
|
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/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
||||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8 h1:vVouagbdmqTVlCIAxpyYsNNTbkKZ3V66VpKOLU/s6W4=
|
|
||||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
|
||||||
github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
|
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/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
||||||
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
|
||||||
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
|
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/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||||
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
|
||||||
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
|
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
|
||||||
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
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 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.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 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
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/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 h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||||
@@ -69,26 +136,16 @@ github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpc
|
|||||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
|
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/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
||||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o=
|
|
||||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
|
||||||
github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
|
||||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
|
|
||||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
|
||||||
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
|
||||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
|
||||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
|
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks=
|
||||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||||
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
|
github.com/golangci/golangci-lint v1.35.2 h1:hD1999/sq3tCPXhhI4UpunxpAAdH9pK7kDIObqoGuWA=
|
||||||
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
github.com/golangci/golangci-lint v1.35.2/go.mod h1:Sg5fFp5oLLI1B8gXfUVUSePju8XF0uWefMkuZuGIHUo=
|
||||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc=
|
|
||||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
|
|
||||||
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
|
||||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI=
|
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI=
|
||||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
||||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
|
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA=
|
||||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||||
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/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 h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||||
@@ -101,180 +158,541 @@ github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs
|
|||||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
|
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/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 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
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/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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
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/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/gookit/color v1.3.1/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/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
|
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
|
||||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
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/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/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.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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 h1:7nkB9fLPMwtn/R6qfPcHileL/x9ydlhw8XyDrLI1ZXg=
|
||||||
|
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
|
||||||
|
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a h1:GmsqmapfzSJkm28dhRoHz2tLRbJmqhU86IPgBtN3mmk=
|
||||||
|
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
|
||||||
|
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3 h1:jNYPNLe3d8smommaoQlK7LOA5ESyUJJ+Wf79ZtA7Vp4=
|
||||||
|
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
|
||||||
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
|
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/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/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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
|
||||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
|
||||||
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.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/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.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/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.2.1 h1:H4rSHiB3ALx//SXr+k9OPqKoOw2cAZpIQwVNH1RL5T4=
|
||||||
|
github.com/kulti/thelper v0.2.1/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/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/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.0/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-20190911065817-5d6d842e92eb h1:RHba4YImhrUVQDHUCe2BNSOz4tVy2yGyXhvYDvxGgeE=
|
||||||
|
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
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 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
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-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
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.1.0 h1:4ykwscnAFeHJruT+EY3M3vdeP8uXMh0VV2E61iR7XD8=
|
||||||
|
github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
||||||
|
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.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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
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 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
|
github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4=
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k=
|
||||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/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-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||||
|
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/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4=
|
||||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/ginkgo v1.14.1/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.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
|
||||||
|
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
|
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
|
||||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
||||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
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 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
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 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/quasilyte/go-ruleguard v0.2.1-0.20201030093329-408e96760278 h1:5gcJ7tORNCNB2QjOJF+MYjzS9aiWpxhP3gntf7RVrOQ=
|
||||||
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/quasilyte/go-ruleguard v0.2.1-0.20201030093329-408e96760278/go.mod h1:2RT/tf0Ce0UDj5y243iWKosQogJd8+1G3Rs2fxmlYnw=
|
||||||
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
|
||||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||||
|
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.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
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/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
|
github.com/securego/gosec/v2 v2.5.0 h1:kjfXLeKdk98gBe2+eYRFMpC4+mxmQQtbidpiiOQ69Qc=
|
||||||
|
github.com/securego/gosec/v2 v2.5.0/go.mod h1:L/CDXVntIff5ypVHIkqPXbtRpJiNCh6c6Amn68jXDjo=
|
||||||
|
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 v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
|
||||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
|
||||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
||||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
|
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
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/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
|
||||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
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/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
|
github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
|
||||||
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
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.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/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/stretchr/testify v1.6.1/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/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2 h1:Xr9gkxfOP0KQWXKNqmwe8vEeSUiUj4Rlee9CMVX2ZUQ=
|
||||||
|
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||||
|
github.com/tetafro/godot v1.3.2 h1:HzWC3XjadkyeuBZxkfAFNY20UVvle0YD51I6zf6RKlU=
|
||||||
|
github.com/tetafro/godot v1.3.2/go.mod h1:ah7jjYmOMnIjS9ku2krapvGQrFNtTLo9Z/qB3dGU1eU=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/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-20200807122107-df9e8bcb914d h1:3EZyvNUMsGD1QA8cu0STNn1L7I77rvhf2IhOcHYQhSw=
|
||||||
|
github.com/tomarrell/wrapcheck v0.0.0-20200807122107-df9e8bcb914d/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
|
||||||
|
github.com/tommy-muehle/go-mnd v1.3.1-0.20201008215730-16041ac3fe65 h1:Y0bLA422kvb32uZI4fy/Plop/Tbld0l9pSzl+j1FWok=
|
||||||
|
github.com/tommy-muehle/go-mnd v1.3.1-0.20201008215730-16041ac3fe65/go.mod h1:T22e7iRN4LsFPZGyRLRXeF+DWVXFuV9thsyO7NjbbTI=
|
||||||
|
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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
||||||
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
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/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
|
||||||
|
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.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=
|
||||||
|
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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/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/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/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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/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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-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-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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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-20190904154756-749cb33beabd/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM=
|
||||||
|
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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/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-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/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/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-20181205014116-22934f0fdb62/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-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/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-20190213192042-740235f6c0d8 h1:b0PLhFjEMqrIqsD4rS5sp0YxBYgdqKzX0KkkGGKLV8I=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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-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-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-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
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-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
|
||||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
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-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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
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-20200410194907-79a7a3126eef/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-20200426102838-f3a5411a4c3b/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-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-20200831203904-5a2aa26beb65/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-20201007032633-0806396f153e/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-20201030010431-2feb2bb1ff51/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-20210105210202-9ed45478a130 h1:8qSBr5nyKsEgkP918Pu5FFDZpTtLIjXSo6mrtdVOFfk=
|
||||||
|
golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
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.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
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.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||||
|
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 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||||
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
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.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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/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=
|
||||||
|
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.6 h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY=
|
||||||
|
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 h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
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 h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
|
||||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||||
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
|
||||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
|
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
|
||||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
|
||||||
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
|
|
||||||
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
|
||||||
|
|||||||
+7
-3
@@ -1,11 +1,15 @@
|
|||||||
|
//go:build tools
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
// the lines are parsed by lazy.sh, do not edit
|
||||||
import (
|
import (
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
|
||||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
|
||||||
_ "github.com/alvaroloes/enumer"
|
_ "github.com/alvaroloes/enumer"
|
||||||
_ "golang.org/x/tools/cmd/goimports"
|
|
||||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
|
_ "github.com/wadey/gocovmerge"
|
||||||
|
_ "golang.org/x/tools/cmd/goimports"
|
||||||
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
|
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
||||||
|
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||||
)
|
)
|
||||||
|
|||||||
+54
-21
@@ -1,12 +1,15 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,29 +22,55 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "One-stop ZFS replication solution",
|
Short: "One-stop ZFS replication solution",
|
||||||
}
|
}
|
||||||
|
|
||||||
var bashcompCmd = &cobra.Command{
|
func init() {
|
||||||
Use: "bashcomp path/to/out/file",
|
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||||
Short: "generate bash completions",
|
}
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) != 1 {
|
var genCompletionCmd = &cobra.Command{
|
||||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
Use: "gencompletion",
|
||||||
err := cmd.Usage()
|
Short: "generate shell auto-completions",
|
||||||
if err != nil {
|
}
|
||||||
panic(err)
|
|
||||||
}
|
type completionCmdInfo struct {
|
||||||
os.Exit(1)
|
genFunc func(outpath string) error
|
||||||
}
|
help string
|
||||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
}
|
||||||
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
|
|
||||||
os.Exit(1)
|
var completionCmdMap = map[string]completionCmdInfo{
|
||||||
}
|
"zsh": {
|
||||||
|
rootCmd.GenZshCompletionFile,
|
||||||
|
" save to file `_zrepl` in your zsh's $fpath",
|
||||||
|
},
|
||||||
|
"bash": {
|
||||||
|
rootCmd.GenBashCompletionFile,
|
||||||
|
" save to a path and source that path in your .bashrc",
|
||||||
},
|
},
|
||||||
Hidden: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
for sh, info := range completionCmdMap {
|
||||||
rootCmd.AddCommand(bashcompCmd)
|
sh, info := sh, info
|
||||||
|
genCompletionCmd.AddCommand(&cobra.Command{
|
||||||
|
Use: fmt.Sprintf("%s path/to/out/file", sh),
|
||||||
|
Short: fmt.Sprintf("generate %s completions", sh),
|
||||||
|
Example: info.help,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||||
|
err := cmd.Usage()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := info.genFunc(args[0]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rootCmd.AddCommand(genCompletionCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subcommand struct {
|
type Subcommand struct {
|
||||||
@@ -49,7 +78,7 @@ type Subcommand struct {
|
|||||||
Short string
|
Short string
|
||||||
Example string
|
Example string
|
||||||
NoRequireConfig bool
|
NoRequireConfig bool
|
||||||
Run func(subcommand *Subcommand, args []string) error
|
Run func(ctx context.Context, subcommand *Subcommand, args []string) error
|
||||||
SetupFlags func(f *pflag.FlagSet)
|
SetupFlags func(f *pflag.FlagSet)
|
||||||
SetupSubcommands func() []*Subcommand
|
SetupSubcommands func() []*Subcommand
|
||||||
|
|
||||||
@@ -70,7 +99,11 @@ func (s *Subcommand) Config() *config.Config {
|
|||||||
|
|
||||||
func (s *Subcommand) run(cmd *cobra.Command, args []string) {
|
func (s *Subcommand) run(cmd *cobra.Command, args []string) {
|
||||||
s.tryParseConfig()
|
s.tryParseConfig()
|
||||||
err := s.Run(s, args)
|
ctx := context.Background()
|
||||||
|
endTask := trace.WithTaskFromStackUpdateCtx(&ctx)
|
||||||
|
defer endTask()
|
||||||
|
err := s.Run(ctx, s, args)
|
||||||
|
endTask()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -29,7 +30,7 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
||||||
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
||||||
},
|
},
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
formatMap := map[string]func(interface{}){
|
formatMap := map[string]func(interface{}){
|
||||||
"": func(i interface{}) {},
|
"": func(i interface{}) {},
|
||||||
"pretty": func(i interface{}) {
|
"pretty": func(i interface{}) {
|
||||||
|
|||||||
+167
-3
@@ -4,9 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/kr/pretty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
@@ -31,20 +35,26 @@ var migrations = []*cli.Subcommand{
|
|||||||
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "replication-cursor:v1-v2",
|
||||||
|
Run: doMigrateReplicationCursor,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&migrateReplicationCursorArgs.dryRun, "dry-run", false, "dry run")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var migratePlaceholder0_1Args struct {
|
var migratePlaceholder0_1Args struct {
|
||||||
dryRun bool
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
func doMigratePlaceholder0_1(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return fmt.Errorf("migration does not take arguments, got %v", args)
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := sc.Config()
|
cfg := sc.Config()
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot list filesystems")
|
return errors.Wrap(err, "cannot list filesystems")
|
||||||
@@ -88,7 +98,7 @@ func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
for _, fs := range wi.fss {
|
for _, fs := range wi.fss {
|
||||||
fmt.Printf("\t%q ... ", fs.ToString())
|
fmt.Printf("\t%q ... ", fs.ToString())
|
||||||
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(fs, migratePlaceholder0_1Args.dryRun)
|
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(ctx, fs, migratePlaceholder0_1Args.dryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error: %s\n", err)
|
fmt.Printf("error: %s\n", err)
|
||||||
} else if !r.NeedsModification {
|
} else if !r.NeedsModification {
|
||||||
@@ -102,3 +112,157 @@ func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var migrateReplicationCursorArgs struct {
|
||||||
|
dryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var bold = color.New(color.Bold)
|
||||||
|
var succ = color.New(color.FgGreen)
|
||||||
|
var fail = color.New(color.FgRed)
|
||||||
|
|
||||||
|
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
|
||||||
|
|
||||||
|
func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := sc.Config()
|
||||||
|
jobs, err := job.JobsFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||||
|
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||||
|
return fmt.Errorf("exiting migration after error")
|
||||||
|
}
|
||||||
|
|
||||||
|
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
|
||||||
|
for i, j := range cfg.Jobs {
|
||||||
|
if jobs[i].Name() != j.Name() {
|
||||||
|
panic("implementation error")
|
||||||
|
}
|
||||||
|
switch j.Ret.(type) {
|
||||||
|
case *config.PushJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
case *config.SourceJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
default:
|
||||||
|
fmt.Printf("ignoring job %q (%d/%d, type %T), not supposed to create v1 replication cursors\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan all filesystems for v1 replication cursors
|
||||||
|
|
||||||
|
fss, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "list filesystems")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hadError bool
|
||||||
|
for _, fs := range fss {
|
||||||
|
|
||||||
|
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
|
||||||
|
|
||||||
|
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
||||||
|
if err == migrateReplicationCursorSkipSentinel {
|
||||||
|
bold.Printf("FILESYSTEM SKIPPED\n")
|
||||||
|
} else if err != nil {
|
||||||
|
hadError = true
|
||||||
|
fail.Printf("MIGRATION FAILED: %s\n", err)
|
||||||
|
} else {
|
||||||
|
succ.Printf("FILESYSTEM %q COMPLETE\n", fs.ToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hadError {
|
||||||
|
fail.Printf("\n\none or more filesystems could not be migrated, please inspect output and or re-run migration")
|
||||||
|
return errors.Errorf("")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, fs *zfs.DatasetPath) error {
|
||||||
|
|
||||||
|
var owningJob job.Job = nil
|
||||||
|
for _, job := range v1CursorJobs {
|
||||||
|
conf := job.SenderConfig()
|
||||||
|
if conf == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pass, err := conf.FSF.Filter(fs)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "filesystem filter error in job %q for fs %q", job.Name(), fs.ToString())
|
||||||
|
}
|
||||||
|
if !pass {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if owningJob != nil {
|
||||||
|
return errors.Errorf("jobs %q and %q both match %q\ncannot attribute replication cursor to either one", owningJob.Name(), job.Name(), fs)
|
||||||
|
}
|
||||||
|
owningJob = job
|
||||||
|
}
|
||||||
|
if owningJob == nil {
|
||||||
|
fmt.Printf("no job's Filesystems filter matches\n")
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||||
|
|
||||||
|
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
|
||||||
|
Types: zfs.Bookmarks,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldCursor *zfs.FilesystemVersion
|
||||||
|
for i, fsv := range bookmarks {
|
||||||
|
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
||||||
|
if err != endpoint.ErrV1ReplicationCursor {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor != nil {
|
||||||
|
fmt.Printf("unexpected v1 replication cursor candidate: %q", fsv.ToAbsPath(fs))
|
||||||
|
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldCursor = &bookmarks[i]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor == nil {
|
||||||
|
bold.Printf("no v1 replication cursor found for filesystem %q\n", fs.ToString())
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("found v1 replication cursor:\n%s\n", pretty.Sprint(oldCursor))
|
||||||
|
|
||||||
|
mostRecentNew, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs.ToString(), owningJob.SenderConfig().JobID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "get most recent v2 replication cursor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mostRecentNew == nil {
|
||||||
|
return errors.Errorf("no v2 replication cursor found for job %q on filesystem %q", owningJob.SenderConfig().JobID, fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("most recent v2 replication cursor:\n%#v", oldCursor)
|
||||||
|
|
||||||
|
if !(mostRecentNew.CreateTXG >= oldCursor.CreateTXG) {
|
||||||
|
return errors.Errorf("v1 replication cursor createtxg is higher than v2 cursor's, skipping this filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("determined that v2 cursor is bookmark of same or newer version than v1 cursor\n")
|
||||||
|
fmt.Printf("destroying v1 cursor %q\n", oldCursor.ToAbsPath(fs))
|
||||||
|
|
||||||
|
if migrateReplicationCursorArgs.dryRun {
|
||||||
|
succ.Printf("DRY RUN\n")
|
||||||
|
} else {
|
||||||
|
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+4
-61
@@ -1,67 +1,10 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import "github.com/zrepl/zrepl/cli"
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
)
|
|
||||||
|
|
||||||
var pprofArgs struct {
|
|
||||||
daemon.PprofServerControlMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
var PprofCmd = &cli.Subcommand{
|
var PprofCmd = &cli.Subcommand{
|
||||||
Use: "pprof off | [on TCP_LISTEN_ADDRESS]",
|
Use: "pprof",
|
||||||
Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS",
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
return []*cli.Subcommand{PprofListenCmd, pprofActivityTraceCmd}
|
||||||
if len(args) < 1 {
|
|
||||||
goto enargs
|
|
||||||
}
|
|
||||||
switch args[0] {
|
|
||||||
case "on":
|
|
||||||
pprofArgs.Run = true
|
|
||||||
if len(args) != 2 {
|
|
||||||
return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument")
|
|
||||||
}
|
|
||||||
pprofArgs.HttpListenAddress = args[1]
|
|
||||||
case "off":
|
|
||||||
if len(args) != 1 {
|
|
||||||
goto enargs
|
|
||||||
}
|
|
||||||
pprofArgs.Run = false
|
|
||||||
}
|
|
||||||
|
|
||||||
RunPProf(subcommand.Config())
|
|
||||||
return nil
|
|
||||||
enargs:
|
|
||||||
return errors.New("invalid number of positional arguments")
|
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunPProf(conf *config.Config) {
|
|
||||||
log := log.New(os.Stderr, "", 0)
|
|
||||||
|
|
||||||
die := func() {
|
|
||||||
log.Printf("exiting after error")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("connecting to zrepl daemon")
|
|
||||||
|
|
||||||
httpc, err := controlHttpClient(conf.Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error creating http client: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, pprofArgs.PprofServerControlMsg, struct{}{})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error sending control message: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
log.Printf("finished")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pprofActivityTraceCmd = &cli.Subcommand{
|
||||||
|
Use: "activity-trace ZREPL_PPROF_HOST:ZREPL_PPROF_PORT",
|
||||||
|
Short: "attach to zrepl daemon with activated pprof listener and dump an activity-trace to stdout",
|
||||||
|
Run: runPProfActivityTrace,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPProfActivityTrace(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
log := log.New(os.Stderr, "", 0)
|
||||||
|
|
||||||
|
die := func() {
|
||||||
|
log.Printf("exiting after error")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) != 1 {
|
||||||
|
log.Printf("exactly one positional argument is required")
|
||||||
|
die()
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "ws://" + args[0] + "/debug/zrepl/activity-trace" // FIXME dont' repeat that
|
||||||
|
|
||||||
|
log.Printf("attaching to activity trace stream %s", url)
|
||||||
|
ws, err := websocket.Dial(url, "", url)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error: %s", err)
|
||||||
|
die()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(os.Stdout, ws)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pprofListenCmd struct {
|
||||||
|
daemon.PprofServerControlMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
var PprofListenCmd = &cli.Subcommand{
|
||||||
|
Use: "listen off | [on TCP_LISTEN_ADDRESS]",
|
||||||
|
Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS",
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
goto enargs
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "on":
|
||||||
|
pprofListenCmd.Run = true
|
||||||
|
if len(args) != 2 {
|
||||||
|
return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument")
|
||||||
|
}
|
||||||
|
pprofListenCmd.HttpListenAddress = args[1]
|
||||||
|
case "off":
|
||||||
|
if len(args) != 1 {
|
||||||
|
goto enargs
|
||||||
|
}
|
||||||
|
pprofListenCmd.Run = false
|
||||||
|
}
|
||||||
|
|
||||||
|
RunPProf(subcommand.Config())
|
||||||
|
return nil
|
||||||
|
enargs:
|
||||||
|
return errors.New("invalid number of positional arguments")
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunPProf(conf *config.Config) {
|
||||||
|
log := log.New(os.Stderr, "", 0)
|
||||||
|
|
||||||
|
die := func() {
|
||||||
|
log.Printf("exiting after error")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("connecting to zrepl daemon")
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(conf.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error creating http client: %s", err)
|
||||||
|
die()
|
||||||
|
}
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, pprofListenCmd.PprofServerControlMsg, struct{}{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error sending control message: %s", err)
|
||||||
|
die()
|
||||||
|
}
|
||||||
|
log.Printf("finished")
|
||||||
|
}
|
||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
@@ -11,7 +13,7 @@ import (
|
|||||||
var SignalCmd = &cli.Subcommand{
|
var SignalCmd = &cli.Subcommand{
|
||||||
Use: "signal [wakeup|reset] JOB",
|
Use: "signal [wakeup|reset] JOB",
|
||||||
Short: "wake up a job from wait state or abort its current invocation",
|
Short: "wake up a job from wait state or abort its current invocation",
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
return runSignalCmd(subcommand.Config(), args)
|
return runSignalCmd(subcommand.Config(), args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,757 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
// tcell is the termbox-compatbile library for abstracting away escape sequences, etc.
|
|
||||||
// as of tcell#252, the number of default distributed terminals is relatively limited
|
|
||||||
// additional terminal definitions can be included via side-effect import
|
|
||||||
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
|
|
||||||
// See https://github.com/gdamore/tcell/issues/252#issuecomment-533836078
|
|
||||||
"github.com/gdamore/tcell/termbox"
|
|
||||||
_ "github.com/gdamore/tcell/terminfo/s/screen" // tmux on FreeBSD 11 & 12 without ncurses
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
|
||||||
)
|
|
||||||
|
|
||||||
type byteProgressMeasurement struct {
|
|
||||||
time time.Time
|
|
||||||
val int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type bytesProgressHistory struct {
|
|
||||||
last *byteProgressMeasurement // pointer as poor man's optional
|
|
||||||
changeCount int
|
|
||||||
lastChange time.Time
|
|
||||||
bpsAvg float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
|
||||||
|
|
||||||
if p.last == nil {
|
|
||||||
p.last = &byteProgressMeasurement{
|
|
||||||
time: time.Now(),
|
|
||||||
val: currentVal,
|
|
||||||
}
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.last.val != currentVal {
|
|
||||||
p.changeCount++
|
|
||||||
p.lastChange = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Since(p.lastChange) > 3*time.Second {
|
|
||||||
p.last = nil
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
deltaV := currentVal - p.last.val
|
|
||||||
deltaT := time.Since(p.last.time)
|
|
||||||
rate := float64(deltaV) / deltaT.Seconds()
|
|
||||||
|
|
||||||
factor := 0.3
|
|
||||||
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
|
||||||
|
|
||||||
p.last.time = time.Now()
|
|
||||||
p.last.val = currentVal
|
|
||||||
|
|
||||||
return int64(p.bpsAvg), p.changeCount
|
|
||||||
}
|
|
||||||
|
|
||||||
type tui struct {
|
|
||||||
x, y int
|
|
||||||
indent int
|
|
||||||
|
|
||||||
lock sync.Mutex //For report and error
|
|
||||||
report map[string]job.Status
|
|
||||||
err error
|
|
||||||
|
|
||||||
jobFilter string
|
|
||||||
|
|
||||||
replicationProgress map[string]*bytesProgressHistory // by job name
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTui() tui {
|
|
||||||
return tui{
|
|
||||||
replicationProgress: make(map[string]*bytesProgressHistory),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const INDENT_MULTIPLIER = 4
|
|
||||||
|
|
||||||
func (t *tui) moveLine(dl int, col int) {
|
|
||||||
t.y += dl
|
|
||||||
t.x = t.indent*INDENT_MULTIPLIER + col
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) write(text string) {
|
|
||||||
for _, c := range text {
|
|
||||||
if c == '\n' {
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
termbox.SetCell(t.x, t.y, c, termbox.ColorDefault, termbox.ColorDefault)
|
|
||||||
t.x += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) printf(text string, a ...interface{}) {
|
|
||||||
t.write(fmt.Sprintf(text, a...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrap(s string, width int) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for len(s) > 0 {
|
|
||||||
rem := width
|
|
||||||
if rem > len(s) {
|
|
||||||
rem = len(s)
|
|
||||||
}
|
|
||||||
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
|
|
||||||
rem = idx + 1
|
|
||||||
}
|
|
||||||
untilNewline := strings.TrimRight(s[:rem], "\n\r")
|
|
||||||
s = s[rem:]
|
|
||||||
if len(untilNewline) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b.WriteString(untilNewline)
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
return strings.TrimRight(b.String(), "\n\r")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
|
|
||||||
whole := fmt.Sprintf(format, a...)
|
|
||||||
width, _ := termbox.Size()
|
|
||||||
if !strings.ContainsAny(whole, "\n\r") && t.x+len(whole) <= width {
|
|
||||||
t.printf(format, a...)
|
|
||||||
} else {
|
|
||||||
t.addIndent(1)
|
|
||||||
t.newline()
|
|
||||||
t.write(wrap(whole, width-INDENT_MULTIPLIER*t.indent))
|
|
||||||
t.addIndent(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) newline() {
|
|
||||||
t.moveLine(1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) setIndent(indent int) {
|
|
||||||
t.indent = indent
|
|
||||||
t.moveLine(0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) addIndent(indent int) {
|
|
||||||
t.indent += indent
|
|
||||||
t.moveLine(0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusFlags struct {
|
|
||||||
Raw bool
|
|
||||||
Job string
|
|
||||||
}
|
|
||||||
|
|
||||||
var StatusCmd = &cli.Subcommand{
|
|
||||||
Use: "status",
|
|
||||||
Short: "show job activity or dump as JSON for monitoring",
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
|
||||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
|
||||||
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
|
||||||
},
|
|
||||||
Run: runStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
func runStatus(s *cli.Subcommand, args []string) error {
|
|
||||||
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusFlags.Raw {
|
|
||||||
resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
|
||||||
_, err := io.CopyN(os.Stderr, resp.Body, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return errors.Errorf("exit")
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t := newTui()
|
|
||||||
t.lock.Lock()
|
|
||||||
t.err = errors.New("Got no report yet")
|
|
||||||
t.lock.Unlock()
|
|
||||||
t.jobFilter = statusFlags.Job
|
|
||||||
|
|
||||||
err = termbox.Init()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer termbox.Close()
|
|
||||||
|
|
||||||
update := func() {
|
|
||||||
m := make(map[string]job.Status)
|
|
||||||
|
|
||||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
|
||||||
struct{}{},
|
|
||||||
&m,
|
|
||||||
)
|
|
||||||
|
|
||||||
t.lock.Lock()
|
|
||||||
t.err = err2
|
|
||||||
t.report = m
|
|
||||||
t.lock.Unlock()
|
|
||||||
t.draw()
|
|
||||||
}
|
|
||||||
update()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
|
||||||
defer ticker.Stop()
|
|
||||||
go func() {
|
|
||||||
for range ticker.C {
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
termbox.HideCursor()
|
|
||||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
switch ev := termbox.PollEvent(); ev.Type {
|
|
||||||
case termbox.EventKey:
|
|
||||||
switch ev.Key {
|
|
||||||
case termbox.KeyEsc:
|
|
||||||
break loop
|
|
||||||
case termbox.KeyCtrlC:
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
case termbox.EventResize:
|
|
||||||
t.draw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
|
|
||||||
p, ok := t.replicationProgress[jobName]
|
|
||||||
if !ok {
|
|
||||||
p = &bytesProgressHistory{}
|
|
||||||
t.replicationProgress[jobName] = p
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) draw() {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
|
||||||
t.x = 0
|
|
||||||
t.y = 0
|
|
||||||
t.indent = 0
|
|
||||||
|
|
||||||
if t.err != nil {
|
|
||||||
t.write(t.err.Error())
|
|
||||||
} else {
|
|
||||||
//Iterate over map in alphabetical order
|
|
||||||
keys := make([]string, 0, len(t.report))
|
|
||||||
for k := range t.report {
|
|
||||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if t.jobFilter != "" && k != t.jobFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
if len(keys) == 0 {
|
|
||||||
t.setIndent(0)
|
|
||||||
t.printf("no jobs to display")
|
|
||||||
t.newline()
|
|
||||||
termbox.Flush()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
v := t.report[k]
|
|
||||||
|
|
||||||
t.setIndent(0)
|
|
||||||
|
|
||||||
t.printf("Job: %s", k)
|
|
||||||
t.setIndent(1)
|
|
||||||
t.newline()
|
|
||||||
t.printf("Type: %s", v.Type)
|
|
||||||
t.setIndent(1)
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
|
||||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
|
||||||
if !ok || activeStatus == nil {
|
|
||||||
t.printf("ActiveSideStatus is null")
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.printf("Replication:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
t.printf("Pruning Sender:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(activeStatus.PruningSender)
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
t.printf("Pruning Receiver:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(activeStatus.PruningReceiver)
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
if v.Type == job.TypePush {
|
|
||||||
t.printf("Snapshotting:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderSnapperReport(activeStatus.Snapshotting)
|
|
||||||
t.addIndent(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if v.Type == job.TypeSnap {
|
|
||||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
|
||||||
if !ok || snapStatus == nil {
|
|
||||||
t.printf("SnapJobStatus is null")
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.printf("Pruning snapshots:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(snapStatus.Pruning)
|
|
||||||
t.addIndent(-1)
|
|
||||||
t.printf("Snapshotting:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderSnapperReport(snapStatus.Snapshotting)
|
|
||||||
t.addIndent(-1)
|
|
||||||
} else if v.Type == job.TypeSource {
|
|
||||||
|
|
||||||
st := v.JobSpecific.(*job.PassiveStatus)
|
|
||||||
t.printf("Snapshotting:\n")
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderSnapperReport(st.Snapper)
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
|
||||||
t.newline()
|
|
||||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
|
||||||
if err != nil {
|
|
||||||
t.printf("Error marshaling status to YAML: %s", err)
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.write(string(asYaml))
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
termbox.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
|
|
||||||
if rep == nil {
|
|
||||||
t.printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if rep.WaitReconnectError != nil {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
if !rep.WaitReconnectSince.IsZero() {
|
|
||||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
|
||||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
|
||||||
var until string
|
|
||||||
if rep.WaitReconnectUntil.IsZero() {
|
|
||||||
until = "waiting indefinitely"
|
|
||||||
} else {
|
|
||||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
|
||||||
rep.WaitReconnectSince, until)
|
|
||||||
} else {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
|
||||||
if len(rep.Attempts) == 0 {
|
|
||||||
t.printf("no attempts made yet")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
t.printf("Attempt #%d", len(rep.Attempts))
|
|
||||||
if len(rep.Attempts) > 1 {
|
|
||||||
t.printf(". Previous attempts failed with the following statuses:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
t.addIndent(-1)
|
|
||||||
} else {
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
|
||||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
|
||||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
|
||||||
})
|
|
||||||
|
|
||||||
t.printf("Status: %s", latest.State)
|
|
||||||
t.newline()
|
|
||||||
if latest.State == report.AttemptPlanningError {
|
|
||||||
t.printf("Problem: ")
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
|
||||||
t.newline()
|
|
||||||
} else if latest.State == report.AttemptFanOutError {
|
|
||||||
t.printf("Problem: one or more of the filesystems encountered errors")
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
|
||||||
// Draw global progress bar
|
|
||||||
// Progress: [---------------]
|
|
||||||
expected, replicated := latest.BytesSum()
|
|
||||||
rate, changeCount := history.Update(replicated)
|
|
||||||
t.write("Progress: ")
|
|
||||||
t.drawBar(50, replicated, expected, changeCount)
|
|
||||||
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
var maxFSLen int
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
if len(fs.Info.Name) > maxFSLen {
|
|
||||||
maxFSLen = len(fs.Info.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|
||||||
if r == nil {
|
|
||||||
t.printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := pruner.StateString(r.State)
|
|
||||||
if err != nil {
|
|
||||||
t.printf("Status: %q (parse error: %q)\n", r.State, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.printf("Status: %s", state)
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
if r.Error != "" {
|
|
||||||
t.printf("Error: %s\n", r.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type commonFS struct {
|
|
||||||
*pruner.FSReport
|
|
||||||
completed bool
|
|
||||||
}
|
|
||||||
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
|
||||||
for i := range r.Pending {
|
|
||||||
all = append(all, commonFS{&r.Pending[i], false})
|
|
||||||
}
|
|
||||||
for i := range r.Completed {
|
|
||||||
all = append(all, commonFS{&r.Completed[i], true})
|
|
||||||
}
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case pruner.Plan:
|
|
||||||
fallthrough
|
|
||||||
case pruner.PlanErr:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(all) == 0 {
|
|
||||||
t.printf("nothing to do\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalDestroyCount, completedDestroyCount int
|
|
||||||
var maxFSname int
|
|
||||||
for _, fs := range all {
|
|
||||||
totalDestroyCount += len(fs.DestroyList)
|
|
||||||
if fs.completed {
|
|
||||||
completedDestroyCount += len(fs.DestroyList)
|
|
||||||
}
|
|
||||||
if maxFSname < len(fs.Filesystem) {
|
|
||||||
maxFSname = len(fs.Filesystem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// global progress bar
|
|
||||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
|
||||||
t.write("Progress: ")
|
|
||||||
t.write("[")
|
|
||||||
t.write(times("=", progress))
|
|
||||||
t.write(">")
|
|
||||||
t.write(times("-", 80-progress))
|
|
||||||
t.write("]")
|
|
||||||
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
sort.SliceStable(all, func(i, j int) bool {
|
|
||||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Draw a table-like representation of 'all'
|
|
||||||
for _, fs := range all {
|
|
||||||
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
|
||||||
t.write(" ")
|
|
||||||
if !fs.SkipReason.NotSkipped() {
|
|
||||||
t.printf("skipped: %s\n", fs.SkipReason)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fs.LastError != "" {
|
|
||||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
|
||||||
t.printf("ERROR:")
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
|
||||||
} else {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
|
||||||
}
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
|
||||||
len(fs.DestroyList), len(fs.SnapshotList))
|
|
||||||
|
|
||||||
if fs.completed {
|
|
||||||
t.printf("Completed %s\n", pruneRuleActionStr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.write("Pending ") // whitespace is padding 10
|
|
||||||
if len(fs.DestroyList) == 1 {
|
|
||||||
t.write(fs.DestroyList[0].Name)
|
|
||||||
} else {
|
|
||||||
t.write(pruneRuleActionStr)
|
|
||||||
}
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
|
||||||
if r == nil {
|
|
||||||
t.printf("<snapshot type does not have a report>\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.printf("Status: %s", r.State)
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
if r.Error != "" {
|
|
||||||
t.printf("Error: %s\n", r.Error)
|
|
||||||
}
|
|
||||||
if !r.SleepUntil.IsZero() {
|
|
||||||
t.printf("Sleep until: %s\n", r.SleepUntil)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(r.Progress, func(i, j int) bool {
|
|
||||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
t.addIndent(1)
|
|
||||||
defer t.addIndent(-1)
|
|
||||||
dur := func(d time.Duration) string {
|
|
||||||
return d.Round(100 * time.Millisecond).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type row struct {
|
|
||||||
path, state, duration, remainder, hookReport string
|
|
||||||
}
|
|
||||||
var widths struct {
|
|
||||||
path, state, duration int
|
|
||||||
}
|
|
||||||
rows := make([]*row, len(r.Progress))
|
|
||||||
for i, fs := range r.Progress {
|
|
||||||
r := &row{
|
|
||||||
path: fs.Path,
|
|
||||||
state: fs.State.String(),
|
|
||||||
}
|
|
||||||
if fs.HooksHadError {
|
|
||||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
|
||||||
}
|
|
||||||
switch fs.State {
|
|
||||||
case snapper.SnapPending:
|
|
||||||
r.duration = "..."
|
|
||||||
r.remainder = ""
|
|
||||||
case snapper.SnapStarted:
|
|
||||||
r.duration = dur(time.Since(fs.StartAt))
|
|
||||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
|
||||||
case snapper.SnapDone:
|
|
||||||
fallthrough
|
|
||||||
case snapper.SnapError:
|
|
||||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
|
||||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
|
||||||
}
|
|
||||||
rows[i] = r
|
|
||||||
if len(r.path) > widths.path {
|
|
||||||
widths.path = len(r.path)
|
|
||||||
}
|
|
||||||
if len(r.state) > widths.state {
|
|
||||||
widths.state = len(r.state)
|
|
||||||
}
|
|
||||||
if len(r.duration) > widths.duration {
|
|
||||||
widths.duration = len(r.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range rows {
|
|
||||||
path := rightPad(r.path, widths.path, " ")
|
|
||||||
state := rightPad(r.state, widths.state, " ")
|
|
||||||
duration := rightPad(r.duration, widths.duration, " ")
|
|
||||||
t.printf("%s %s %s", path, state, duration)
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
|
||||||
if r.hookReport != "" {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
|
||||||
}
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func times(str string, n int) (out string) {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
out += str
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func rightPad(str string, length int, pad string) string {
|
|
||||||
if len(str) > length {
|
|
||||||
return str[:length]
|
|
||||||
}
|
|
||||||
return str + strings.Repeat(pad, length-len(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
var arrowPositions = `>\|/`
|
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progresss
|
|
||||||
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
|
||||||
var completedLength int
|
|
||||||
if totalBytes > 0 {
|
|
||||||
completedLength = int(int64(length) * bytes / totalBytes)
|
|
||||||
if completedLength > length {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
} else if totalBytes == bytes {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
|
|
||||||
t.write("[")
|
|
||||||
t.write(times("=", completedLength))
|
|
||||||
t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
|
||||||
t.write(times("-", length-completedLength))
|
|
||||||
t.write("]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
|
||||||
|
|
||||||
expected, replicated := rep.BytesSum()
|
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)",
|
|
||||||
strings.ToUpper(string(rep.State)),
|
|
||||||
rep.CurrentStep, len(rep.Steps),
|
|
||||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
|
||||||
)
|
|
||||||
|
|
||||||
activeIndicator := " "
|
|
||||||
if active {
|
|
||||||
activeIndicator = "*"
|
|
||||||
}
|
|
||||||
t.printf("%s %s %s ",
|
|
||||||
activeIndicator,
|
|
||||||
rightPad(rep.Info.Name, maxFS, " "),
|
|
||||||
status)
|
|
||||||
|
|
||||||
next := ""
|
|
||||||
if err := rep.Error(); err != nil {
|
|
||||||
next = err.Err
|
|
||||||
} else if rep.State != report.FilesystemDone {
|
|
||||||
if nextStep := rep.NextStep(); nextStep != nil {
|
|
||||||
if nextStep.IsIncremental() {
|
|
||||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
|
||||||
} else {
|
|
||||||
next = fmt.Sprintf("next: %s (full)", nextStep.Info.To)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next = "" // individual FSes may still be in planning state
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
|
|
||||||
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ByteCountBinary(b int64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if b < unit {
|
|
||||||
return fmt.Sprintf("%d B", b)
|
|
||||||
}
|
|
||||||
div, exp := int64(unit), 0
|
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
h http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(network, addr string) (*Client, error) {
|
||||||
|
httpc, err := controlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Client{httpc}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Status() (s daemon.Status, _ error) {
|
||||||
|
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus,
|
||||||
|
struct{}{},
|
||||||
|
&s,
|
||||||
|
)
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) StatusRaw() ([]byte, error) {
|
||||||
|
var r json.RawMessage
|
||||||
|
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus, struct{}{}, &r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) signal(job, sig string) error {
|
||||||
|
return jsonRequestResponse(c.h, daemon.ControlJobEndpointSignal,
|
||||||
|
struct {
|
||||||
|
Name string
|
||||||
|
Op string
|
||||||
|
}{
|
||||||
|
Name: job,
|
||||||
|
Op: sig,
|
||||||
|
},
|
||||||
|
struct{}{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SignalWakeup(job string) error {
|
||||||
|
return c.signal(job, "wakeup")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SignalReset(job string) error {
|
||||||
|
return c.signal(job, "reset")
|
||||||
|
}
|
||||||
|
|
||||||
|
func controlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client http.Client, err error) {
|
||||||
|
return http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return dialfunc(ctx)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonRequestResponse(c http.Client, endpoint string, req interface{}, res interface{}) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
encodeErr := json.NewEncoder(&buf).Encode(req)
|
||||||
|
if encodeErr != nil {
|
||||||
|
return encodeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Post("http://unix"+endpoint, "application/json", &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var msg bytes.Buffer
|
||||||
|
_, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
|
||||||
|
return errors.Errorf("%s", msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeError := json.NewDecoder(resp.Body).Decode(&res)
|
||||||
|
if decodeError != nil {
|
||||||
|
return decodeError
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/client/status/client"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/util/choices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
Status() (daemon.Status, error)
|
||||||
|
StatusRaw() ([]byte, error)
|
||||||
|
SignalWakeup(job string) error
|
||||||
|
SignalReset(job string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusFlags struct {
|
||||||
|
Mode choices.Choices
|
||||||
|
Job string
|
||||||
|
Delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusv2Flags statusFlags
|
||||||
|
|
||||||
|
type statusv2Mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusV2ModeInteractive statusv2Mode = 1 + iota
|
||||||
|
StatusV2ModeDump
|
||||||
|
StatusV2ModeRaw
|
||||||
|
StatusV2ModeLegacy
|
||||||
|
)
|
||||||
|
|
||||||
|
var Subcommand = &cli.Subcommand{
|
||||||
|
Use: "status",
|
||||||
|
Short: "retrieve & display daemon status information",
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
statusv2Flags.Mode.Init(
|
||||||
|
"interactive", StatusV2ModeInteractive,
|
||||||
|
"dump", StatusV2ModeDump,
|
||||||
|
"raw", StatusV2ModeRaw,
|
||||||
|
"legacy", StatusV2ModeLegacy,
|
||||||
|
)
|
||||||
|
statusv2Flags.Mode.SetTypeString("mode")
|
||||||
|
statusv2Flags.Mode.SetDefaultValue(StatusV2ModeInteractive)
|
||||||
|
f.Var(&statusv2Flags.Mode, "mode", statusv2Flags.Mode.Usage())
|
||||||
|
f.StringVar(&statusv2Flags.Job, "job", "", "only show specified job (works in \"dump\" and \"interactive\" mode)")
|
||||||
|
f.DurationVarP(&statusv2Flags.Delay, "delay", "d", 1*time.Second, "use -d 3s for 3 seconds delay (minimum delay is 1s)")
|
||||||
|
},
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
return runStatusV2Command(ctx, subcommand.Config(), args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStatusV2Command(ctx context.Context, config *config.Config, args []string) error {
|
||||||
|
|
||||||
|
c, err := client.New("unix", config.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "connect to daemon socket at %q", config.Global.Control.SockPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
||||||
|
|
||||||
|
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump && mode != StatusV2ModeRaw {
|
||||||
|
dumpmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
rawmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeRaw)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return errors.Errorf("error: stdout is not a tty, please use --mode %s or --mode %s", dumpmode, rawmode)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case StatusV2ModeInteractive:
|
||||||
|
return interactive(c, statusv2Flags)
|
||||||
|
case StatusV2ModeDump:
|
||||||
|
return dump(c, statusv2Flags.Job)
|
||||||
|
case StatusV2ModeRaw:
|
||||||
|
return raw(c)
|
||||||
|
case StatusV2ModeLegacy:
|
||||||
|
return legacy(c, statusv2Flags)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dump(c Client, job string) error {
|
||||||
|
s, err := c.Status()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if job != "" {
|
||||||
|
if _, ok := s.Jobs[job]; !ok {
|
||||||
|
return errors.Errorf("job %q not found", job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width := (1 << 31) - 1
|
||||||
|
wrap := false
|
||||||
|
hline := strings.Repeat("-", 80)
|
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
wrap = true
|
||||||
|
screen, err := tcell.NewScreen()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "get terminal dimensions")
|
||||||
|
}
|
||||||
|
if err := screen.Init(); err != nil {
|
||||||
|
return errors.Wrap(err, "init screen")
|
||||||
|
}
|
||||||
|
width, _ = screen.Size()
|
||||||
|
screen.Fini()
|
||||||
|
hline = strings.Repeat("-", width)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := viewmodel.New()
|
||||||
|
params := viewmodel.Params{
|
||||||
|
Report: s.Jobs,
|
||||||
|
ReportFetchError: nil,
|
||||||
|
SelectedJob: nil,
|
||||||
|
FSFilter: func(s string) bool { return true },
|
||||||
|
DetailViewWidth: width,
|
||||||
|
DetailViewWrap: wrap,
|
||||||
|
ShortKeybindingOverview: "",
|
||||||
|
}
|
||||||
|
m.Update(params)
|
||||||
|
for _, j := range m.Jobs() {
|
||||||
|
if job != "" && j.Name() != job {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
params.SelectedJob = j
|
||||||
|
m.Update(params)
|
||||||
|
fmt.Println(m.SelectedJob().FullDescription())
|
||||||
|
if job != "" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
fmt.Println(hline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
tview "gitlab.com/tslocum/cview"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func interactive(c Client, flag statusFlags) error {
|
||||||
|
|
||||||
|
// Set this so we don't overwrite the default terminal colors
|
||||||
|
// See https://github.com/rivo/tview/blob/master/styles.go
|
||||||
|
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||||
|
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
||||||
|
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
||||||
|
tview.Styles.BorderColor = tcell.ColorDefault
|
||||||
|
app := tview.NewApplication()
|
||||||
|
|
||||||
|
jobDetailSplit := tview.NewFlex()
|
||||||
|
jobMenu := tview.NewTreeView()
|
||||||
|
jobMenuRoot := tview.NewTreeNode("jobs")
|
||||||
|
jobMenuRoot.SetSelectable(true)
|
||||||
|
jobMenu.SetRoot(jobMenuRoot)
|
||||||
|
jobMenu.SetCurrentNode(jobMenuRoot)
|
||||||
|
jobMenu.SetSelectedTextColor(tcell.ColorGreen)
|
||||||
|
jobTextDetail := tview.NewTextView()
|
||||||
|
jobTextDetail.SetWrap(false)
|
||||||
|
|
||||||
|
jobMenu.SetBorder(true)
|
||||||
|
jobTextDetail.SetBorder(true)
|
||||||
|
|
||||||
|
toolbarSplit := tview.NewFlex()
|
||||||
|
toolbarSplit.SetDirection(tview.FlexRow)
|
||||||
|
inputBarContainer := tview.NewFlex()
|
||||||
|
fsFilterInput := tview.NewInputField()
|
||||||
|
fsFilterInput.SetBorder(false)
|
||||||
|
fsFilterInput.SetFieldBackgroundColor(tcell.ColorDefault)
|
||||||
|
inputBarLabel := tview.NewTextView()
|
||||||
|
inputBarLabel.SetText("[::b]FILTER ")
|
||||||
|
inputBarLabel.SetDynamicColors(true)
|
||||||
|
inputBarContainer.AddItem(inputBarLabel, 7, 1, false)
|
||||||
|
inputBarContainer.AddItem(fsFilterInput, 0, 10, false)
|
||||||
|
toolbarSplit.AddItem(inputBarContainer, 1, 0, false)
|
||||||
|
toolbarSplit.AddItem(jobDetailSplit, 0, 10, false)
|
||||||
|
|
||||||
|
bottombar := tview.NewFlex()
|
||||||
|
bottombar.SetDirection(tview.FlexColumn)
|
||||||
|
bottombarDateView := tview.NewTextView()
|
||||||
|
bottombar.AddItem(bottombarDateView, len(time.Now().String()), 0, false)
|
||||||
|
bottomBarStatus := tview.NewTextView()
|
||||||
|
bottomBarStatus.SetDynamicColors(true)
|
||||||
|
bottomBarStatus.SetTextAlign(tview.AlignRight)
|
||||||
|
bottombar.AddItem(bottomBarStatus, 0, 10, false)
|
||||||
|
toolbarSplit.AddItem(bottombar, 1, 0, false)
|
||||||
|
|
||||||
|
tabbableWithJobMenu := []tview.Primitive{jobMenu, jobTextDetail, fsFilterInput}
|
||||||
|
tabbableWithoutJobMenu := []tview.Primitive{jobTextDetail, fsFilterInput}
|
||||||
|
var tabbable []tview.Primitive
|
||||||
|
tabbableActiveIndex := 0
|
||||||
|
tabbableRedraw := func() {
|
||||||
|
if len(tabbable) == 0 {
|
||||||
|
app.SetFocus(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tabbableActiveIndex >= len(tabbable) {
|
||||||
|
app.SetFocus(tabbable[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.SetFocus(tabbable[tabbableActiveIndex])
|
||||||
|
}
|
||||||
|
tabbableCycle := func() {
|
||||||
|
if len(tabbable) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tabbableActiveIndex = (tabbableActiveIndex + 1) % len(tabbable)
|
||||||
|
app.SetFocus(tabbable[tabbableActiveIndex])
|
||||||
|
tabbableRedraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
jobMenuVisisble := false
|
||||||
|
reconfigureJobDetailSplit := func(setJobMenuVisible bool) {
|
||||||
|
if jobMenuVisisble == setJobMenuVisible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jobMenuVisisble = setJobMenuVisible
|
||||||
|
if setJobMenuVisible {
|
||||||
|
jobDetailSplit.RemoveItem(jobTextDetail)
|
||||||
|
jobDetailSplit.AddItem(jobMenu, 0, 1, true)
|
||||||
|
jobDetailSplit.AddItem(jobTextDetail, 0, 5, false)
|
||||||
|
tabbable = tabbableWithJobMenu
|
||||||
|
} else {
|
||||||
|
jobDetailSplit.RemoveItem(jobMenu)
|
||||||
|
tabbable = tabbableWithoutJobMenu
|
||||||
|
}
|
||||||
|
tabbableRedraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal := func(m *tview.Modal, modalDoneFunc func(idx int, label string)) {
|
||||||
|
preModalFocus := app.GetFocus()
|
||||||
|
m.SetDoneFunc(func(idx int, label string) {
|
||||||
|
if modalDoneFunc != nil {
|
||||||
|
modalDoneFunc(idx, label)
|
||||||
|
}
|
||||||
|
app.SetRoot(toolbarSplit, true)
|
||||||
|
app.SetFocus(preModalFocus)
|
||||||
|
app.Draw()
|
||||||
|
})
|
||||||
|
app.SetRoot(m, true)
|
||||||
|
app.Draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.SetRoot(toolbarSplit, true)
|
||||||
|
// initial focus
|
||||||
|
tabbableActiveIndex = len(tabbable)
|
||||||
|
tabbableCycle()
|
||||||
|
reconfigureJobDetailSplit(true)
|
||||||
|
|
||||||
|
m := viewmodel.New()
|
||||||
|
params := &viewmodel.Params{
|
||||||
|
Report: nil,
|
||||||
|
SelectedJob: nil,
|
||||||
|
FSFilter: func(_ string) bool { return true },
|
||||||
|
DetailViewWidth: 100,
|
||||||
|
DetailViewWrap: false,
|
||||||
|
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
||||||
|
}
|
||||||
|
paramsMtx := &sync.Mutex{}
|
||||||
|
var redraw func()
|
||||||
|
viewmodelupdate := func(cb func(*viewmodel.Params)) {
|
||||||
|
paramsMtx.Lock()
|
||||||
|
defer paramsMtx.Unlock()
|
||||||
|
cb(params)
|
||||||
|
m.Update(*params)
|
||||||
|
}
|
||||||
|
redraw = func() {
|
||||||
|
jobs := m.Jobs()
|
||||||
|
if flag.Job != "" {
|
||||||
|
job_found := false
|
||||||
|
for _, job := range jobs {
|
||||||
|
if strings.Compare(flag.Job, job.Name()) == 0 {
|
||||||
|
jobs = []*viewmodel.Job{job}
|
||||||
|
job_found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !job_found {
|
||||||
|
jobs = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redrawJobsList := false
|
||||||
|
var selectedJobN *tview.TreeNode
|
||||||
|
if len(jobMenuRoot.GetChildren()) == len(jobs) {
|
||||||
|
for i, jobN := range jobMenuRoot.GetChildren() {
|
||||||
|
if jobN.GetReference().(*viewmodel.Job) != jobs[i] {
|
||||||
|
redrawJobsList = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if jobN.GetReference().(*viewmodel.Job) == m.SelectedJob() {
|
||||||
|
selectedJobN = jobN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
redrawJobsList = true
|
||||||
|
}
|
||||||
|
if redrawJobsList {
|
||||||
|
selectedJobN = nil
|
||||||
|
children := make([]*tview.TreeNode, len(jobs))
|
||||||
|
for i := range jobs {
|
||||||
|
jobN := tview.NewTreeNode(jobs[i].JobTreeTitle())
|
||||||
|
jobN.SetReference(jobs[i])
|
||||||
|
jobN.SetSelectable(true)
|
||||||
|
children[i] = jobN
|
||||||
|
jobN.SetSelectedFunc(func() {
|
||||||
|
viewmodelupdate(func(p *viewmodel.Params) {
|
||||||
|
p.SelectedJob = jobN.GetReference().(*viewmodel.Job)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if jobs[i] == m.SelectedJob() {
|
||||||
|
selectedJobN = jobN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobMenuRoot.SetChildren(children)
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedJobN != nil && jobMenu.GetCurrentNode() != selectedJobN {
|
||||||
|
jobMenu.SetCurrentNode(selectedJobN)
|
||||||
|
} else if selectedJobN == nil {
|
||||||
|
// select something, otherwise selection breaks (likely bug in tview)
|
||||||
|
jobMenu.SetCurrentNode(jobMenuRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
if selJ := m.SelectedJob(); selJ != nil {
|
||||||
|
jobTextDetail.SetText(selJ.FullDescription())
|
||||||
|
} else {
|
||||||
|
jobTextDetail.SetText("please select a job")
|
||||||
|
}
|
||||||
|
|
||||||
|
bottombardatestring := m.DateString()
|
||||||
|
bottombarDateView.SetText(bottombardatestring)
|
||||||
|
bottombar.ResizeItem(bottombarDateView, len(bottombardatestring), 0)
|
||||||
|
|
||||||
|
bottomBarStatus.SetText(m.BottomBarStatus())
|
||||||
|
|
||||||
|
app.Draw()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
app.Suspend(func() {
|
||||||
|
panic(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
st, err := c.Status()
|
||||||
|
viewmodelupdate(func(p *viewmodel.Params) {
|
||||||
|
p.Report = st.Jobs
|
||||||
|
p.ReportFetchError = err
|
||||||
|
})
|
||||||
|
app.QueueUpdateDraw(redraw)
|
||||||
|
|
||||||
|
time.Sleep(flag.Delay)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
jobMenu.SetChangedFunc(func(jobN *tview.TreeNode) {
|
||||||
|
viewmodelupdate(func(p *viewmodel.Params) {
|
||||||
|
p.SelectedJob, _ = jobN.GetReference().(*viewmodel.Job)
|
||||||
|
})
|
||||||
|
redraw()
|
||||||
|
jobTextDetail.ScrollToBeginning()
|
||||||
|
})
|
||||||
|
jobMenu.SetSelectedFunc(func(jobN *tview.TreeNode) {
|
||||||
|
app.SetFocus(jobTextDetail)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
||||||
|
viewmodelupdate(func(p *viewmodel.Params) {
|
||||||
|
_, _, p.DetailViewWidth, _ = jobTextDetail.GetInnerRect()
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if e.Key() == tcell.KeyTab {
|
||||||
|
tabbableCycle()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Key() == tcell.KeyRune && app.GetFocus() == fsFilterInput {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Key() == tcell.KeyRune && e.Rune() == '/' {
|
||||||
|
if app.GetFocus() != fsFilterInput {
|
||||||
|
app.SetFocus(fsFilterInput)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Key() == tcell.KeyRune && e.Rune() == 'M' {
|
||||||
|
reconfigureJobDetailSplit(!jobMenuVisisble)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Key() == tcell.KeyRune && e.Rune() == 'q' {
|
||||||
|
app.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Key() == tcell.KeyRune && e.Rune() == 'S' {
|
||||||
|
job, ok := jobMenu.GetCurrentNode().GetReference().(*viewmodel.Job)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
signals := []string{"wakeup", "reset"}
|
||||||
|
clientFuncs := []func(job string) error{c.SignalWakeup, c.SignalReset}
|
||||||
|
sigMod := tview.NewModal()
|
||||||
|
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
||||||
|
sigMod.SetBorder(true)
|
||||||
|
sigMod.GetForm().SetButtonTextColorFocused(tcell.ColorGreen)
|
||||||
|
sigMod.AddButtons(signals)
|
||||||
|
sigMod.SetText(fmt.Sprintf("Send a signal to job %q", job.Name()))
|
||||||
|
showModal(sigMod, func(idx int, _ string) {
|
||||||
|
go func() {
|
||||||
|
if idx == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := clientFuncs[idx](job.Name())
|
||||||
|
if err != nil {
|
||||||
|
app.QueueUpdate(func() {
|
||||||
|
me := tview.NewModal()
|
||||||
|
me.SetText(fmt.Sprintf("signal error: %s", err))
|
||||||
|
me.AddButtons([]string{"Close"})
|
||||||
|
showModal(me, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
|
||||||
|
fsFilterInput.SetChangedFunc(func(searchterm string) {
|
||||||
|
viewmodelupdate(func(p *viewmodel.Params) {
|
||||||
|
p.FSFilter = func(fs string) bool {
|
||||||
|
r, err := regexp.Compile(searchterm)
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return r.MatchString(fs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
redraw()
|
||||||
|
jobTextDetail.ScrollToBeginning()
|
||||||
|
})
|
||||||
|
fsFilterInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyEnter {
|
||||||
|
app.SetFocus(jobTextDetail)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
jobTextDetail.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyRune && event.Rune() == 'w' {
|
||||||
|
// toggle wrapping
|
||||||
|
viewmodelupdate(func(p *viewmodel.Params) {
|
||||||
|
p.DetailViewWrap = !p.DetailViewWrap
|
||||||
|
})
|
||||||
|
redraw()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
return app.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
tview "gitlab.com/tslocum/cview"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func legacy(c Client, flag statusFlags) error {
|
||||||
|
|
||||||
|
// Set this so we don't overwrite the default terminal colors
|
||||||
|
// See https://github.com/rivo/tview/blob/master/styles.go
|
||||||
|
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||||
|
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
||||||
|
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
||||||
|
tview.Styles.BorderColor = tcell.ColorDefault
|
||||||
|
app := tview.NewApplication()
|
||||||
|
|
||||||
|
textView := tview.NewTextView()
|
||||||
|
textView.SetWrap(true)
|
||||||
|
textView.SetScrollable(true) // so that it allows us to set scroll position
|
||||||
|
textView.SetScrollBarVisibility(tview.ScrollBarNever)
|
||||||
|
|
||||||
|
app.SetRoot(textView, true)
|
||||||
|
|
||||||
|
width := (1 << 31) - 1
|
||||||
|
wrap := false
|
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
wrap = true
|
||||||
|
screen, err := tcell.NewScreen()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "get terminal dimensions")
|
||||||
|
}
|
||||||
|
if err := screen.Init(); err != nil {
|
||||||
|
return errors.Wrap(err, "init screen")
|
||||||
|
}
|
||||||
|
width, _ = screen.Size()
|
||||||
|
screen.Fini()
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsMtx := &sync.Mutex{}
|
||||||
|
params := viewmodel.Params{
|
||||||
|
Report: nil,
|
||||||
|
ReportFetchError: nil,
|
||||||
|
SelectedJob: nil,
|
||||||
|
FSFilter: func(s string) bool { return true },
|
||||||
|
DetailViewWidth: width,
|
||||||
|
DetailViewWrap: wrap,
|
||||||
|
ShortKeybindingOverview: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw := func() {
|
||||||
|
textView.Clear()
|
||||||
|
|
||||||
|
paramsMtx.Lock()
|
||||||
|
defer paramsMtx.Unlock()
|
||||||
|
|
||||||
|
if params.ReportFetchError != nil {
|
||||||
|
fmt.Fprintln(textView, params.ReportFetchError.Error())
|
||||||
|
} else if params.Report != nil {
|
||||||
|
m := viewmodel.New()
|
||||||
|
m.Update(params)
|
||||||
|
for _, j := range m.Jobs() {
|
||||||
|
if flag.Job != "" && j.Name() != flag.Job {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
params.SelectedJob = j
|
||||||
|
m.Update(params)
|
||||||
|
fmt.Fprintln(textView, m.SelectedJob().FullDescription())
|
||||||
|
if flag.Job != "" {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
hline := strings.Repeat("-", params.DetailViewWidth)
|
||||||
|
fmt.Fprintln(textView, hline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(textView, "waiting for request results")
|
||||||
|
}
|
||||||
|
textView.ScrollToBeginning()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
||||||
|
// sync resizes to `params`
|
||||||
|
paramsMtx.Lock()
|
||||||
|
_, _, newWidth, _ := textView.GetInnerRect()
|
||||||
|
if newWidth != params.DetailViewWidth {
|
||||||
|
params.DetailViewWidth = newWidth
|
||||||
|
app.QueueUpdateDraw(redraw)
|
||||||
|
}
|
||||||
|
paramsMtx.Unlock()
|
||||||
|
|
||||||
|
textView.ScrollToBeginning() // has the effect of inhibiting user scrolls
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
app.Suspend(func() {
|
||||||
|
panic(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
st, err := c.Status()
|
||||||
|
paramsMtx.Lock()
|
||||||
|
params.Report = st.Jobs
|
||||||
|
params.ReportFetchError = err
|
||||||
|
paramsMtx.Unlock()
|
||||||
|
|
||||||
|
app.QueueUpdateDraw(redraw)
|
||||||
|
|
||||||
|
time.Sleep(flag.Delay)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return app.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func raw(c Client) error {
|
||||||
|
b, err := c.StatusRaw()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(os.Stdout, bytes.NewReader(b)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package viewmodel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ByteCountBinaryUint(b uint64) string {
|
||||||
|
if b > math.MaxInt64 {
|
||||||
|
panic(b)
|
||||||
|
}
|
||||||
|
return ByteCountBinary(int64(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ByteCountBinary(b int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if b < unit {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
div, exp := unit, 0
|
||||||
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package viewmodel
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type byteProgressMeasurement struct {
|
||||||
|
time time.Time
|
||||||
|
val uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type bytesProgressHistory struct {
|
||||||
|
last *byteProgressMeasurement // pointer as poor man's optional
|
||||||
|
changeCount int
|
||||||
|
lastChange time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *bytesProgressHistory) Update(currentVal uint64) (bytesPerSecondAvg int64, changeCount int) {
|
||||||
|
|
||||||
|
if p.last == nil {
|
||||||
|
p.last = &byteProgressMeasurement{
|
||||||
|
time: time.Now(),
|
||||||
|
val: currentVal,
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.last.val != currentVal {
|
||||||
|
p.changeCount++
|
||||||
|
p.lastChange = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(p.lastChange) > 3*time.Second {
|
||||||
|
p.last = nil
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var deltaV int64
|
||||||
|
if currentVal >= p.last.val {
|
||||||
|
deltaV = int64(currentVal - p.last.val)
|
||||||
|
} else {
|
||||||
|
deltaV = -int64(p.last.val - currentVal)
|
||||||
|
}
|
||||||
|
deltaT := time.Since(p.last.time)
|
||||||
|
rate := float64(deltaV) / deltaT.Seconds()
|
||||||
|
|
||||||
|
p.last.time = time.Now()
|
||||||
|
p.last.val = currentVal
|
||||||
|
|
||||||
|
return int64(rate), p.changeCount
|
||||||
|
}
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
package viewmodel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
yaml "github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/client/status/viewmodel/stringbuilder"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
type M struct {
|
||||||
|
jobs map[string]*Job
|
||||||
|
jobsList []*Job
|
||||||
|
selectedJob *Job
|
||||||
|
dateString string
|
||||||
|
bottomBarStatus string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
// long-lived
|
||||||
|
name string
|
||||||
|
byteProgress *bytesProgressHistory
|
||||||
|
|
||||||
|
lastStatus *job.Status
|
||||||
|
fulldescription string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *M {
|
||||||
|
return &M{
|
||||||
|
jobs: make(map[string]*Job),
|
||||||
|
jobsList: make([]*Job, 0),
|
||||||
|
selectedJob: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterFunc func(string) bool
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
Report map[string]*job.Status
|
||||||
|
ReportFetchError error
|
||||||
|
SelectedJob *Job
|
||||||
|
FSFilter FilterFunc `validate:"required"`
|
||||||
|
DetailViewWidth int `validate:"gte=1"`
|
||||||
|
DetailViewWrap bool
|
||||||
|
ShortKeybindingOverview string
|
||||||
|
}
|
||||||
|
|
||||||
|
var validate = validator.New()
|
||||||
|
|
||||||
|
func (m *M) Update(p Params) {
|
||||||
|
|
||||||
|
if err := validate.Struct(p); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ReportFetchError != nil {
|
||||||
|
m.bottomBarStatus = fmt.Sprintf("[red::]status fetch: %s", p.ReportFetchError)
|
||||||
|
} else {
|
||||||
|
m.bottomBarStatus = p.ShortKeybindingOverview
|
||||||
|
for jobname, st := range p.Report {
|
||||||
|
// TODO handle job renames & deletions
|
||||||
|
j, ok := m.jobs[jobname]
|
||||||
|
if !ok {
|
||||||
|
j = &Job{
|
||||||
|
name: jobname,
|
||||||
|
byteProgress: &bytesProgressHistory{},
|
||||||
|
}
|
||||||
|
m.jobs[jobname] = j
|
||||||
|
m.jobsList = append(m.jobsList, j)
|
||||||
|
}
|
||||||
|
j.lastStatus = st
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out internal jobs
|
||||||
|
var jobsList []*Job
|
||||||
|
for _, j := range m.jobsList {
|
||||||
|
if daemon.IsInternalJobName(j.name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
jobsList = append(jobsList, j)
|
||||||
|
}
|
||||||
|
m.jobsList = jobsList
|
||||||
|
|
||||||
|
// determinism!
|
||||||
|
sort.Slice(m.jobsList, func(i, j int) bool {
|
||||||
|
return strings.Compare(m.jobsList[i].name, m.jobsList[j].name) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// try to not lose the selected job
|
||||||
|
m.selectedJob = nil
|
||||||
|
for _, j := range m.jobsList {
|
||||||
|
j.updateFullDescription(p)
|
||||||
|
if j == p.SelectedJob {
|
||||||
|
m.selectedJob = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dateString = time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *M) BottomBarStatus() string { return m.bottomBarStatus }
|
||||||
|
|
||||||
|
func (m *M) Jobs() []*Job { return m.jobsList }
|
||||||
|
|
||||||
|
// may be nil
|
||||||
|
func (m *M) SelectedJob() *Job { return m.selectedJob }
|
||||||
|
|
||||||
|
func (m *M) DateString() string { return m.dateString }
|
||||||
|
|
||||||
|
func (j *Job) updateFullDescription(p Params) {
|
||||||
|
width := p.DetailViewWidth
|
||||||
|
if !p.DetailViewWrap {
|
||||||
|
width = 10000000 // FIXME
|
||||||
|
}
|
||||||
|
b := stringbuilder.New(stringbuilder.Config{
|
||||||
|
IndentMultiplier: 3,
|
||||||
|
Width: width,
|
||||||
|
})
|
||||||
|
drawJob(b, j.name, j.lastStatus, j.byteProgress, p.FSFilter)
|
||||||
|
j.fulldescription = b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) JobTreeTitle() string {
|
||||||
|
return j.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) FullDescription() string {
|
||||||
|
return j.fulldescription
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) Name() string {
|
||||||
|
return j.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawJob(t *stringbuilder.B, name string, v *job.Status, history *bytesProgressHistory, fsfilter FilterFunc) {
|
||||||
|
|
||||||
|
t.Printf("Job: %s\n", name)
|
||||||
|
t.Printf("Type: %s\n\n", v.Type)
|
||||||
|
|
||||||
|
if v.Type == job.TypePush || v.Type == job.TypePull {
|
||||||
|
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
||||||
|
if !ok || activeStatus == nil {
|
||||||
|
t.Printf("ActiveSideStatus is null")
|
||||||
|
t.Newline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Printf("Replication:")
|
||||||
|
t.AddIndentAndNewline(1)
|
||||||
|
renderReplicationReport(t, activeStatus.Replication, history, fsfilter)
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
|
||||||
|
t.Printf("Pruning Sender:")
|
||||||
|
t.AddIndentAndNewline(1)
|
||||||
|
renderPrunerReport(t, activeStatus.PruningSender, fsfilter)
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
|
||||||
|
t.Printf("Pruning Receiver:")
|
||||||
|
t.AddIndentAndNewline(1)
|
||||||
|
renderPrunerReport(t, activeStatus.PruningReceiver, fsfilter)
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
|
||||||
|
if v.Type == job.TypePush {
|
||||||
|
t.Printf("Snapshotting:")
|
||||||
|
t.AddIndentAndNewline(1)
|
||||||
|
renderSnapperReport(t, activeStatus.Snapshotting, fsfilter)
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if v.Type == job.TypeSnap {
|
||||||
|
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||||
|
if !ok || snapStatus == nil {
|
||||||
|
t.Printf("SnapJobStatus is null")
|
||||||
|
t.Newline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Printf("Pruning snapshots:")
|
||||||
|
t.AddIndentAndNewline(1)
|
||||||
|
renderPrunerReport(t, snapStatus.Pruning, fsfilter)
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
t.Printf("Snapshotting:")
|
||||||
|
t.AddIndentAndNewline(1)
|
||||||
|
renderSnapperReport(t, snapStatus.Snapshotting, fsfilter)
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
} else if v.Type == job.TypeSource {
|
||||||
|
|
||||||
|
st := v.JobSpecific.(*job.PassiveStatus)
|
||||||
|
t.Printf("Snapshotting:\n")
|
||||||
|
t.AddIndent(1)
|
||||||
|
renderSnapperReport(t, st.Snapper, fsfilter)
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
t.Printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||||
|
t.Newline()
|
||||||
|
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||||
|
if err != nil {
|
||||||
|
t.Printf("Error marshaling status to YAML: %s", err)
|
||||||
|
t.Newline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Write(string(asYaml))
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, active bool, maxFS int) {
|
||||||
|
|
||||||
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
|
sizeEstimationImpreciseNotice := ""
|
||||||
|
if containsInvalidSizeEstimates {
|
||||||
|
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||||
|
}
|
||||||
|
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||||
|
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||||
|
}
|
||||||
|
|
||||||
|
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||||
|
strings.ToUpper(string(rep.State)),
|
||||||
|
rep.CurrentStep, len(rep.Steps),
|
||||||
|
ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected),
|
||||||
|
sizeEstimationImpreciseNotice,
|
||||||
|
)
|
||||||
|
|
||||||
|
activeIndicator := " "
|
||||||
|
if active {
|
||||||
|
activeIndicator = "*"
|
||||||
|
}
|
||||||
|
t.AddIndent(1)
|
||||||
|
t.Printf("%s %s %s ",
|
||||||
|
activeIndicator,
|
||||||
|
stringbuilder.RightPad(rep.Info.Name, maxFS, " "),
|
||||||
|
status)
|
||||||
|
|
||||||
|
next := ""
|
||||||
|
if err := rep.Error(); err != nil {
|
||||||
|
next = err.Err
|
||||||
|
} else if rep.State != report.FilesystemDone {
|
||||||
|
if nextStep := rep.NextStep(); nextStep != nil {
|
||||||
|
if nextStep.IsIncremental() {
|
||||||
|
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||||
|
} else {
|
||||||
|
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||||
|
}
|
||||||
|
attribs := []string{}
|
||||||
|
|
||||||
|
if nextStep.Info.Resumed {
|
||||||
|
attribs = append(attribs, "resumed")
|
||||||
|
}
|
||||||
|
|
||||||
|
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||||
|
|
||||||
|
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||||
|
} else {
|
||||||
|
next = "" // individual FSes may still be in planning state
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
t.Printf("%s", next)
|
||||||
|
|
||||||
|
t.AddIndent(-1)
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *bytesProgressHistory, fsfilter FilterFunc) {
|
||||||
|
if rep == nil {
|
||||||
|
t.Printf("...\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rep.WaitReconnectError != nil {
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
if !rep.WaitReconnectSince.IsZero() {
|
||||||
|
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||||
|
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||||
|
var until string
|
||||||
|
if rep.WaitReconnectUntil.IsZero() {
|
||||||
|
until = "waiting indefinitely"
|
||||||
|
} else {
|
||||||
|
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
||||||
|
}
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
||||||
|
rep.WaitReconnectSince, until)
|
||||||
|
} else {
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
||||||
|
}
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO visualize more than the latest attempt by folding all attempts into one
|
||||||
|
if len(rep.Attempts) == 0 {
|
||||||
|
t.Printf("no attempts made yet")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
t.Printf("Attempt #%d", len(rep.Attempts))
|
||||||
|
if len(rep.Attempts) > 1 {
|
||||||
|
t.Printf(". Previous attempts failed with the following statuses:")
|
||||||
|
t.AddIndentAndNewline(1)
|
||||||
|
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)\n", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||||
|
}
|
||||||
|
t.AddIndentAndNewline(-1)
|
||||||
|
} else {
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latest := rep.Attempts[len(rep.Attempts)-1]
|
||||||
|
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
||||||
|
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
||||||
|
})
|
||||||
|
|
||||||
|
// apply filter
|
||||||
|
filtered := make([]*report.FilesystemReport, 0, len(latest.Filesystems))
|
||||||
|
for _, fs := range latest.Filesystems {
|
||||||
|
if !fsfilter(fs.Info.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, fs)
|
||||||
|
}
|
||||||
|
latest.Filesystems = filtered
|
||||||
|
|
||||||
|
t.Printf("Status: %s", latest.State)
|
||||||
|
t.Newline()
|
||||||
|
if !latest.FinishAt.IsZero() {
|
||||||
|
t.Printf("Last Run: %s (lasted %s)\n", latest.FinishAt.Round(time.Second), latest.FinishAt.Sub(latest.StartAt).Round(time.Second))
|
||||||
|
} else {
|
||||||
|
t.Printf("Started: %s (lasting %s)\n", latest.StartAt.Round(time.Second), time.Since(latest.StartAt).Round(time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest.State == report.AttemptPlanningError {
|
||||||
|
t.Printf("Problem: ")
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
||||||
|
t.Newline()
|
||||||
|
} else if latest.State == report.AttemptFanOutError {
|
||||||
|
t.Printf("Problem: one or more of the filesystems encountered errors")
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||||
|
// Draw global progress bar
|
||||||
|
// Progress: [---------------]
|
||||||
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
|
rate, changeCount := history.Update(replicated)
|
||||||
|
eta := time.Duration(0)
|
||||||
|
if rate > 0 {
|
||||||
|
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Write("Progress: ")
|
||||||
|
t.DrawBar(50, replicated, expected, changeCount)
|
||||||
|
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
||||||
|
if eta != 0 {
|
||||||
|
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||||
|
}
|
||||||
|
t.Newline()
|
||||||
|
if containsInvalidSizeEstimates {
|
||||||
|
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(latest.Filesystems) == 0 {
|
||||||
|
t.Write("NOTE: no filesystems were considered for replication!")
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxFSLen int
|
||||||
|
for _, fs := range latest.Filesystems {
|
||||||
|
if len(fs.Info.Name) > maxFSLen {
|
||||||
|
maxFSLen = len(fs.Info.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fs := range latest.Filesystems {
|
||||||
|
printFilesystemStatus(t, fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanizeDuration(duration time.Duration) string {
|
||||||
|
days := int64(duration.Hours() / 24)
|
||||||
|
hours := int64(math.Mod(duration.Hours(), 24))
|
||||||
|
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||||
|
seconds := int64(math.Mod(duration.Seconds(), 60))
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
force := false
|
||||||
|
chunks := []int64{days, hours, minutes, seconds}
|
||||||
|
for i, chunk := range chunks {
|
||||||
|
if force || chunk > 0 {
|
||||||
|
padding := 0
|
||||||
|
if force {
|
||||||
|
padding = 2
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
||||||
|
if r == nil {
|
||||||
|
t.Printf("...\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := pruner.StateString(r.State)
|
||||||
|
if err != nil {
|
||||||
|
t.Printf("Status: %q (parse error: %q)\n", r.State, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Printf("Status: %s", state)
|
||||||
|
t.Newline()
|
||||||
|
|
||||||
|
if r.Error != "" {
|
||||||
|
t.Printf("Error: %s\n", r.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commonFS struct {
|
||||||
|
*pruner.FSReport
|
||||||
|
completed bool
|
||||||
|
}
|
||||||
|
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||||
|
for i := range r.Pending {
|
||||||
|
all = append(all, commonFS{&r.Pending[i], false})
|
||||||
|
}
|
||||||
|
for i := range r.Completed {
|
||||||
|
all = append(all, commonFS{&r.Completed[i], true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter all
|
||||||
|
filtered := make([]commonFS, 0, len(all))
|
||||||
|
for _, fs := range all {
|
||||||
|
if fsfilter(fs.FSReport.Filesystem) {
|
||||||
|
filtered = append(filtered, fs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all = filtered
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case pruner.Plan:
|
||||||
|
fallthrough
|
||||||
|
case pruner.PlanErr:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(all) == 0 {
|
||||||
|
t.Printf("nothing to do\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalDestroyCount, completedDestroyCount int
|
||||||
|
var maxFSname int
|
||||||
|
for _, fs := range all {
|
||||||
|
totalDestroyCount += len(fs.DestroyList)
|
||||||
|
if fs.completed {
|
||||||
|
completedDestroyCount += len(fs.DestroyList)
|
||||||
|
}
|
||||||
|
if maxFSname < len(fs.Filesystem) {
|
||||||
|
maxFSname = len(fs.Filesystem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// global progress bar
|
||||||
|
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||||
|
t.Write("Progress: ")
|
||||||
|
t.Write("[")
|
||||||
|
t.Write(stringbuilder.Times("=", progress))
|
||||||
|
t.Write(">")
|
||||||
|
t.Write(stringbuilder.Times("-", 80-progress))
|
||||||
|
t.Write("]")
|
||||||
|
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||||
|
t.Newline()
|
||||||
|
|
||||||
|
sort.SliceStable(all, func(i, j int) bool {
|
||||||
|
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw a table-like representation of 'all'
|
||||||
|
for _, fs := range all {
|
||||||
|
t.Write(stringbuilder.RightPad(fs.Filesystem, maxFSname, " "))
|
||||||
|
t.Write(" ")
|
||||||
|
if !fs.SkipReason.NotSkipped() {
|
||||||
|
t.Printf("skipped: %s\n", fs.SkipReason)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fs.LastError != "" {
|
||||||
|
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||||
|
t.Printf("ERROR:")
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||||
|
} else {
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||||
|
}
|
||||||
|
t.Newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
||||||
|
len(fs.DestroyList), len(fs.SnapshotList))
|
||||||
|
|
||||||
|
if fs.completed {
|
||||||
|
t.Printf("Completed %s\n", pruneRuleActionStr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Write("Pending ") // whitespace is padding 10
|
||||||
|
if len(fs.DestroyList) == 1 {
|
||||||
|
t.Write(fs.DestroyList[0].Name)
|
||||||
|
} else {
|
||||||
|
t.Write(pruneRuleActionStr)
|
||||||
|
}
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
||||||
|
if r == nil {
|
||||||
|
t.Printf("<snapshot type does not have a report>\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Printf("Status: %s", r.State)
|
||||||
|
t.Newline()
|
||||||
|
|
||||||
|
if r.Error != "" {
|
||||||
|
t.Printf("Error: %s\n", r.Error)
|
||||||
|
}
|
||||||
|
if !r.SleepUntil.IsZero() {
|
||||||
|
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(r.Progress, func(i, j int) bool {
|
||||||
|
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
dur := func(d time.Duration) string {
|
||||||
|
return d.Round(100 * time.Millisecond).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
path, state, duration, remainder, hookReport string
|
||||||
|
}
|
||||||
|
var widths struct {
|
||||||
|
path, state, duration int
|
||||||
|
}
|
||||||
|
rows := make([]*row, 0, len(r.Progress))
|
||||||
|
for _, fs := range r.Progress {
|
||||||
|
if !fsfilter(fs.Path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := &row{
|
||||||
|
path: fs.Path,
|
||||||
|
state: fs.State.String(),
|
||||||
|
}
|
||||||
|
if fs.HooksHadError {
|
||||||
|
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||||
|
}
|
||||||
|
switch fs.State {
|
||||||
|
case snapper.SnapPending:
|
||||||
|
r.duration = "..."
|
||||||
|
r.remainder = ""
|
||||||
|
case snapper.SnapStarted:
|
||||||
|
r.duration = dur(time.Since(fs.StartAt))
|
||||||
|
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||||
|
case snapper.SnapDone:
|
||||||
|
fallthrough
|
||||||
|
case snapper.SnapError:
|
||||||
|
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||||
|
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||||
|
}
|
||||||
|
rows = append(rows, r)
|
||||||
|
if len(r.path) > widths.path {
|
||||||
|
widths.path = len(r.path)
|
||||||
|
}
|
||||||
|
if len(r.state) > widths.state {
|
||||||
|
widths.state = len(r.state)
|
||||||
|
}
|
||||||
|
if len(r.duration) > widths.duration {
|
||||||
|
widths.duration = len(r.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rows {
|
||||||
|
path := stringbuilder.RightPad(r.path, widths.path, " ")
|
||||||
|
state := stringbuilder.RightPad(r.state, widths.state, " ")
|
||||||
|
duration := stringbuilder.RightPad(r.duration, widths.duration, " ")
|
||||||
|
t.Printf("%s %s %s", path, state, duration)
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||||
|
if r.hookReport != "" {
|
||||||
|
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||||
|
}
|
||||||
|
t.Newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package stringbuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type B struct {
|
||||||
|
// const
|
||||||
|
indentMultiplier int
|
||||||
|
|
||||||
|
// mut
|
||||||
|
sb *strings.Builder
|
||||||
|
indent int
|
||||||
|
width int
|
||||||
|
x, y int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
IndentMultiplier int `validate:"gte=1"`
|
||||||
|
Width int `validate:"gte=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var validate = validator.New()
|
||||||
|
|
||||||
|
func New(config Config) *B {
|
||||||
|
|
||||||
|
if err := validate.Struct(config); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &B{sb: &strings.Builder{}, width: config.Width, indentMultiplier: config.IndentMultiplier}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *B) String() string { return b.sb.String() }
|
||||||
|
|
||||||
|
func (w *B) Newline() {
|
||||||
|
w.Write("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *B) PrintfDrawIndentedAndWrappedIfMultiline(format string, args ...interface{}) {
|
||||||
|
whole := fmt.Sprintf(format, args...)
|
||||||
|
if strings.ContainsAny(whole, "\n\r") {
|
||||||
|
w.AddIndent(1)
|
||||||
|
defer w.AddIndent(-1)
|
||||||
|
}
|
||||||
|
w.Write(whole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *B) Printf(format string, args ...interface{}) {
|
||||||
|
whole := fmt.Sprintf(format, args...)
|
||||||
|
w.Write(whole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *B) AddIndent(delta int) {
|
||||||
|
t.indent += delta * t.indentMultiplier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *B) AddIndentAndNewline(delta int) {
|
||||||
|
t.indent += delta * t.indentMultiplier
|
||||||
|
t.Write("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *B) Write(s string) {
|
||||||
|
for _, c := range s {
|
||||||
|
if c == '\n' {
|
||||||
|
fmt.Fprint(w.sb, "\n")
|
||||||
|
w.x = 0
|
||||||
|
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
||||||
|
w.x = w.indent
|
||||||
|
w.y++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if w.x >= w.width {
|
||||||
|
fmt.Fprint(w.sb, "\n")
|
||||||
|
w.x = 0
|
||||||
|
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
||||||
|
w.x = w.indent
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w.sb, "%c", c)
|
||||||
|
w.x++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Times(str string, n int) (out string) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out += str
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func RightPad(str string, length int, pad string) string {
|
||||||
|
if len(str) > length {
|
||||||
|
return str[:length]
|
||||||
|
}
|
||||||
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeCount = 0 indicates stall / no progress
|
||||||
|
func (w *B) DrawBar(length int, bytes, totalBytes uint64, changeCount int) {
|
||||||
|
const arrowPositions = `>\|/`
|
||||||
|
var completedLength int
|
||||||
|
if totalBytes > 0 {
|
||||||
|
completedLength = int(uint64(length) * bytes / totalBytes)
|
||||||
|
if completedLength > length {
|
||||||
|
completedLength = length
|
||||||
|
}
|
||||||
|
} else if totalBytes == bytes {
|
||||||
|
completedLength = length
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write("[")
|
||||||
|
w.Write(Times("=", completedLength))
|
||||||
|
w.Write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||||
|
w.Write(Times("-", length-completedLength))
|
||||||
|
w.Write("]")
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
var StdinserverCmd = &cli.Subcommand{
|
var StdinserverCmd = &cli.Subcommand{
|
||||||
Use: "stdinserver CLIENT_IDENTITY",
|
Use: "stdinserver CLIENT_IDENTITY",
|
||||||
Short: "stdinserver transport mode (started from authorized_keys file as forced command)",
|
Short: "stdinserver transport mode (started from authorized_keys file as forced command)",
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
return runStdinserver(subcommand.Config(), args)
|
return runStdinserver(subcommand.Config(), args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-6
@@ -1,7 +1,10 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@@ -15,7 +18,7 @@ import (
|
|||||||
var TestCmd = &cli.Subcommand{
|
var TestCmd = &cli.Subcommand{
|
||||||
Use: "test",
|
Use: "test",
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
return []*cli.Subcommand{testFilter, testPlaceholder}
|
return []*cli.Subcommand{testFilter, testPlaceholder, testDecodeResumeToken}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ var testFilter = &cli.Subcommand{
|
|||||||
Run: runTestFilterCmd,
|
Run: runTestFilterCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
func runTestFilterCmd(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
if testFilterArgs.job == "" {
|
if testFilterArgs.job == "" {
|
||||||
return fmt.Errorf("must specify --job flag")
|
return fmt.Errorf("must specify --job flag")
|
||||||
@@ -57,6 +60,8 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
confFilter = j.Filesystems
|
confFilter = j.Filesystems
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
confFilter = j.Filesystems
|
confFilter = j.Filesystems
|
||||||
|
case *config.SnapJob:
|
||||||
|
confFilter = j.Filesystems
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
||||||
}
|
}
|
||||||
@@ -70,7 +75,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
if testFilterArgs.input != "" {
|
if testFilterArgs.input != "" {
|
||||||
fsnames = []string{testFilterArgs.input}
|
fsnames = []string{testFilterArgs.input}
|
||||||
} else {
|
} else {
|
||||||
out, err := zfs.ZFSList([]string{"name"})
|
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not list ZFS filesystems: %s", err)
|
return fmt.Errorf("could not list ZFS filesystems: %s", err)
|
||||||
}
|
}
|
||||||
@@ -131,13 +136,13 @@ var testPlaceholder = &cli.Subcommand{
|
|||||||
Run: runTestPlaceholder,
|
Run: runTestPlaceholder,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var checkDPs []*zfs.DatasetPath
|
var checkDPs []*zfs.DatasetPath
|
||||||
|
|
||||||
// all actions first
|
// all actions first
|
||||||
if testPlaceholderArgs.all {
|
if testPlaceholderArgs.all {
|
||||||
out, err := zfs.ZFSList([]string{"name"})
|
out, err := zfs.ZFSList(ctx, []string{"name"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not list ZFS filesystems")
|
return errors.Wrap(err, "could not list ZFS filesystems")
|
||||||
}
|
}
|
||||||
@@ -161,7 +166,7 @@ func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
||||||
for _, dp := range checkDPs {
|
for _, dp := range checkDPs {
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(dp)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, dp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot get placeholder state")
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
}
|
}
|
||||||
@@ -176,3 +181,32 @@ func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testDecodeResumeTokenArgs struct {
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
var testDecodeResumeToken = &cli.Subcommand{
|
||||||
|
Use: "decoderesumetoken --token TOKEN",
|
||||||
|
Short: "decode resume token",
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.StringVar(&testDecodeResumeTokenArgs.token, "token", "", "the resume token obtained from the receive_resume_token property")
|
||||||
|
},
|
||||||
|
Run: runTestDecodeResumeTokenCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestDecodeResumeTokenCmd(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
if testDecodeResumeTokenArgs.token == "" {
|
||||||
|
return fmt.Errorf("token argument must be specified")
|
||||||
|
}
|
||||||
|
token, err := zfs.ParseResumeToken(ctx, testDecodeResumeTokenArgs.token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err := enc.Encode(&token); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ var VersionCmd = &cli.Subcommand{
|
|||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)")
|
f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)")
|
||||||
},
|
},
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
versionArgs.Config = subcommand.Config()
|
versionArgs.Config = subcommand.Config()
|
||||||
versionArgs.ConfigErr = subcommand.ConfigParsingError()
|
versionArgs.ConfigErr = subcommand.ConfigParsingError()
|
||||||
return runVersionCmd()
|
return runVersionCmd()
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ZFSAbstractionsCmd = &cli.Subcommand{
|
||||||
|
Use: "zfs-abstraction",
|
||||||
|
Short: "manage abstractions that zrepl builds on top of ZFS",
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return []*cli.Subcommand{
|
||||||
|
zabsCmdList,
|
||||||
|
zabsCmdReleaseAll,
|
||||||
|
zabsCmdReleaseStale,
|
||||||
|
zabsCmdCreate,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// a common set of CLI flags that map to the fields of an
|
||||||
|
// endpoint.ListZFSHoldsAndBookmarksQuery
|
||||||
|
type zabsFilterFlags struct {
|
||||||
|
Filesystems FilesystemsFilterFlag
|
||||||
|
Job JobIDFlag
|
||||||
|
Types AbstractionTypesFlag
|
||||||
|
Concurrency int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// produce a query from the CLI flags
|
||||||
|
func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
|
||||||
|
q := endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||||
|
FS: f.Filesystems.FlagValue(),
|
||||||
|
What: f.Types.FlagValue(),
|
||||||
|
JobID: f.Job.FlagValue(),
|
||||||
|
Concurrency: f.Concurrency,
|
||||||
|
}
|
||||||
|
return q, q.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string) {
|
||||||
|
// Note: the default value is defined in the .FlagValue methods
|
||||||
|
s.Var(&f.Filesystems, "fs", fmt.Sprintf("only %s holds on the specified filesystem [default: all filesystems] [comma-separated list of <dataset-pattern>:<ok|!> pairs]", verb))
|
||||||
|
s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
|
||||||
|
|
||||||
|
variants := make([]string, 0, len(endpoint.AbstractionTypesAll))
|
||||||
|
for v := range endpoint.AbstractionTypesAll {
|
||||||
|
variants = append(variants, string(v))
|
||||||
|
}
|
||||||
|
variants = sort.StringSlice(variants)
|
||||||
|
variantsJoined := strings.Join(variants, "|")
|
||||||
|
s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined))
|
||||||
|
|
||||||
|
s.Int64VarP(&f.Concurrency, "concurrency", "p", 1, "number of concurrently queried filesystems")
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobIDFlag struct{ J *endpoint.JobID }
|
||||||
|
|
||||||
|
func (f *JobIDFlag) Set(s string) error {
|
||||||
|
if len(s) == 0 {
|
||||||
|
*f = JobIDFlag{J: nil}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID, err := endpoint.MakeJobID(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*f = JobIDFlag{J: &jobID}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f JobIDFlag) Type() string { return "job-ID" }
|
||||||
|
func (f JobIDFlag) String() string { return fmt.Sprint(f.J) }
|
||||||
|
func (f JobIDFlag) FlagValue() *endpoint.JobID { return f.J }
|
||||||
|
|
||||||
|
type AbstractionTypesFlag map[endpoint.AbstractionType]bool
|
||||||
|
|
||||||
|
func (f *AbstractionTypesFlag) Set(s string) error {
|
||||||
|
ats, err := endpoint.AbstractionTypeSetFromStrings(strings.Split(s, ","))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*f = AbstractionTypesFlag(ats)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f AbstractionTypesFlag) Type() string { return "abstraction-type" }
|
||||||
|
func (f AbstractionTypesFlag) String() string {
|
||||||
|
return endpoint.AbstractionTypeSet(f).String()
|
||||||
|
}
|
||||||
|
func (f AbstractionTypesFlag) FlagValue() map[endpoint.AbstractionType]bool {
|
||||||
|
if len(f) > 0 {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
return endpoint.AbstractionTypesAll
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesystemsFilterFlag struct {
|
||||||
|
F endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (flag *FilesystemsFilterFlag) Set(s string) error {
|
||||||
|
mappings := strings.Split(s, ",")
|
||||||
|
if len(mappings) == 1 && !strings.Contains(mappings[0], ":") {
|
||||||
|
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||||
|
FS: &mappings[0],
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f := filters.NewDatasetMapFilter(len(mappings), true)
|
||||||
|
for _, m := range mappings {
|
||||||
|
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
|
||||||
|
lhsrhs := strings.SplitN(m, ":", 2)
|
||||||
|
if len(lhsrhs) != 2 {
|
||||||
|
return thisMappingErr
|
||||||
|
}
|
||||||
|
err := f.Add(lhsrhs[0], lhsrhs[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %s", thisMappingErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||||
|
Filter: f,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (flag FilesystemsFilterFlag) Type() string { return "filesystem filter spec" }
|
||||||
|
func (flag FilesystemsFilterFlag) String() string {
|
||||||
|
return fmt.Sprintf("%v", flag.F)
|
||||||
|
}
|
||||||
|
func (flag FilesystemsFilterFlag) FlagValue() endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter {
|
||||||
|
var z FilesystemsFilterFlag
|
||||||
|
if flag == z {
|
||||||
|
return endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{Filter: zfs.NoFilter()}
|
||||||
|
}
|
||||||
|
return flag.F
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import "github.com/zrepl/zrepl/cli"
|
||||||
|
|
||||||
|
var zabsCmdCreate = &cli.Subcommand{
|
||||||
|
Use: "create",
|
||||||
|
NoRequireConfig: true,
|
||||||
|
Short: `create zrepl ZFS abstractions (mostly useful for debugging & development, users should not need to use this command)`,
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return []*cli.Subcommand{
|
||||||
|
zabsCmdCreateStepHold,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var zabsCreateStepHoldFlags struct {
|
||||||
|
target string
|
||||||
|
jobid JobIDFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
var zabsCmdCreateStepHold = &cli.Subcommand{
|
||||||
|
Use: "step",
|
||||||
|
Run: doZabsCreateStep,
|
||||||
|
NoRequireConfig: true,
|
||||||
|
Short: `create a step hold or bookmark`,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.StringVarP(&zabsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
|
||||||
|
f.VarP(&zabsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func doZabsCreateStep(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return errors.New("subcommand takes no arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
f := &zabsCreateStepHoldFlags
|
||||||
|
|
||||||
|
fs, _, _, err := zfs.DecomposeVersionString(f.target)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "%q invalid target", f.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.jobid.FlagValue() == nil {
|
||||||
|
return errors.Errorf("jobid must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := zfs.ZFSGetFilesystemVersion(ctx, f.target)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "get info about target %q", f.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
step, err := endpoint.HoldStep(ctx, fs, v, *f.jobid.FlagValue())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "create step hold")
|
||||||
|
}
|
||||||
|
fmt.Println(step.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var zabsListFlags struct {
|
||||||
|
Filter zabsFilterFlags
|
||||||
|
Json bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var zabsCmdList = &cli.Subcommand{
|
||||||
|
Use: "list",
|
||||||
|
Short: `list zrepl ZFS abstractions`,
|
||||||
|
Run: doZabsList,
|
||||||
|
NoRequireConfig: true,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
zabsListFlags.Filter.registerZabsFilterFlags(f, "list")
|
||||||
|
f.BoolVar(&zabsListFlags.Json, "json", false, "emit JSON")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
return errors.New("this subcommand takes no positional arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := zabsListFlags.Filter.Query()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid filter specification on command line")
|
||||||
|
}
|
||||||
|
|
||||||
|
abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return err // context clear by invocation of command
|
||||||
|
}
|
||||||
|
|
||||||
|
var line chainlock.L
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
// print results
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
for a := range abstractions {
|
||||||
|
func() {
|
||||||
|
defer line.Lock().Unlock()
|
||||||
|
if zabsListFlags.Json {
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err := enc.Encode(a); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
} else {
|
||||||
|
fmt.Println(a)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// print errors to stderr
|
||||||
|
errorColor := color.New(color.FgRed)
|
||||||
|
var errorsSlice []endpoint.ListAbstractionsError
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for err := range errors {
|
||||||
|
func() {
|
||||||
|
defer line.Lock().Unlock()
|
||||||
|
errorsSlice = append(errorsSlice, err)
|
||||||
|
errorColor.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
if len(errorsSlice) > 0 {
|
||||||
|
errorColor.Add(color.Bold).Fprintf(os.Stderr, "there were errors in listing the abstractions")
|
||||||
|
return fmt.Errorf("")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// shared between release-all and release-step
|
||||||
|
var zabsReleaseFlags struct {
|
||||||
|
Filter zabsFilterFlags
|
||||||
|
Json bool
|
||||||
|
DryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerZabsReleaseFlags(s *pflag.FlagSet) {
|
||||||
|
zabsReleaseFlags.Filter.registerZabsFilterFlags(s, "release")
|
||||||
|
s.BoolVar(&zabsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
|
||||||
|
s.BoolVar(&zabsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
|
||||||
|
}
|
||||||
|
|
||||||
|
var zabsCmdReleaseAll = &cli.Subcommand{
|
||||||
|
Use: "release-all",
|
||||||
|
Run: doZabsReleaseAll,
|
||||||
|
NoRequireConfig: true,
|
||||||
|
Short: `(DANGEROUS) release ALL zrepl ZFS abstractions (mostly useful for uninstalling zrepl completely or for "de-zrepl-ing" a filesystem)`,
|
||||||
|
SetupFlags: registerZabsReleaseFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
var zabsCmdReleaseStale = &cli.Subcommand{
|
||||||
|
Use: "release-stale",
|
||||||
|
Run: doZabsReleaseStale,
|
||||||
|
NoRequireConfig: true,
|
||||||
|
Short: `release stale zrepl ZFS abstractions (useful if zrepl has a bug and does not do it by itself)`,
|
||||||
|
SetupFlags: registerZabsReleaseFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func doZabsReleaseAll(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
return errors.New("this subcommand takes no positional arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := zabsReleaseFlags.Filter.Query()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid filter specification on command line")
|
||||||
|
}
|
||||||
|
|
||||||
|
abstractions, listErrors, err := endpoint.ListAbstractions(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return err // context clear by invocation of command
|
||||||
|
}
|
||||||
|
if len(listErrors) > 0 {
|
||||||
|
color.New(color.FgRed).Fprintf(os.Stderr, "there were errors in listing the abstractions:\n%s\n", listErrors)
|
||||||
|
// proceed anyways with rest of abstractions
|
||||||
|
}
|
||||||
|
|
||||||
|
return doZabsRelease_Common(ctx, abstractions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doZabsReleaseStale(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
return errors.New("this subcommand takes no positional arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := zabsReleaseFlags.Filter.Query()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid filter specification on command line")
|
||||||
|
}
|
||||||
|
|
||||||
|
stalenessInfo, err := endpoint.ListStale(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return err // context clear by invocation of command
|
||||||
|
}
|
||||||
|
|
||||||
|
return doZabsRelease_Common(ctx, stalenessInfo.Stale)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
|
||||||
|
|
||||||
|
if zabsReleaseFlags.DryRun {
|
||||||
|
if zabsReleaseFlags.Json {
|
||||||
|
m, err := json.MarshalIndent(destroy, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stdout.Write(m); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
} else {
|
||||||
|
for _, a := range destroy {
|
||||||
|
fmt.Printf("would destroy %s\n", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome := endpoint.BatchDestroy(ctx, destroy)
|
||||||
|
hadErr := false
|
||||||
|
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
colorErr := color.New(color.FgRed)
|
||||||
|
printfSuccess := color.New(color.FgGreen).FprintfFunc()
|
||||||
|
printfSection := color.New(color.Bold).FprintfFunc()
|
||||||
|
|
||||||
|
for res := range outcome {
|
||||||
|
hadErr = hadErr || res.DestroyErr != nil
|
||||||
|
if zabsReleaseFlags.Json {
|
||||||
|
err := enc.Encode(res)
|
||||||
|
if err != nil {
|
||||||
|
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printfSection(os.Stdout, "destroy %s ...", res.Abstraction)
|
||||||
|
if res.DestroyErr != nil {
|
||||||
|
colorErr.Fprintf(os.Stdout, " failed:\n%s\n", res.DestroyErr)
|
||||||
|
} else {
|
||||||
|
printfSuccess(os.Stdout, " OK\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hadErr {
|
||||||
|
colorErr.Add(color.Bold).Fprintf(os.Stderr, "there were errors in destroying the abstractions")
|
||||||
|
return fmt.Errorf("")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
-12
@@ -12,6 +12,9 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/datasizeunit"
|
||||||
|
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -52,11 +55,12 @@ func (j JobEnum) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActiveJob struct {
|
type ActiveJob struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Connect ConnectEnum `yaml:"connect"`
|
Connect ConnectEnum `yaml:"connect"`
|
||||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
|
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PassiveJob struct {
|
type PassiveJob struct {
|
||||||
@@ -75,18 +79,78 @@ type SnapJob struct {
|
|||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendOptions struct {
|
||||||
|
Encrypted bool `yaml:"encrypted,optional,default=false"`
|
||||||
|
Raw bool `yaml:"raw,optional,default=false"`
|
||||||
|
SendProperties bool `yaml:"send_properties,optional,default=false"`
|
||||||
|
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
||||||
|
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
||||||
|
Compressed bool `yaml:"compressed,optional,default=false"`
|
||||||
|
EmbeddedData bool `yaml:"embbeded_data,optional,default=false"`
|
||||||
|
Saved bool `yaml:"saved,optional,default=false"`
|
||||||
|
|
||||||
|
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecvOptions struct {
|
||||||
|
// Note: we cannot enforce encrypted recv as the ZFS cli doesn't provide a mechanism for it
|
||||||
|
// Encrypted bool `yaml:"may_encrypted"`
|
||||||
|
// Future:
|
||||||
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
|
||||||
|
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
||||||
|
|
||||||
|
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Unmarshaler = &datasizeunit.Bits{}
|
||||||
|
|
||||||
|
type BandwidthLimit struct {
|
||||||
|
Max datasizeunit.Bits `yaml:"max,default=-1 B"`
|
||||||
|
BucketCapacity datasizeunit.Bits `yaml:"bucket_capacity,default=128 KiB"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Replication struct {
|
||||||
|
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
|
||||||
|
Concurrency *ReplicationOptionsConcurrency `yaml:"concurrency,optional,fromdefaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplicationOptionsProtection struct {
|
||||||
|
Initial string `yaml:"initial,optional,default=guarantee_resumability"`
|
||||||
|
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplicationOptionsConcurrency struct {
|
||||||
|
Steps int `yaml:"steps,optional,default=1"`
|
||||||
|
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PropertyRecvOptions struct {
|
||||||
|
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
||||||
|
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *PushJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
||||||
|
func (j *PushJob) GetSendOptions() *SendOptions { return j.Send }
|
||||||
|
|
||||||
type PullJob struct {
|
type PullJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
Interval PositiveDurationOrManual `yaml:"interval"`
|
Interval PositiveDurationOrManual `yaml:"interval"`
|
||||||
|
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *PullJob) GetRootFS() string { return j.RootFS }
|
||||||
|
func (j *PullJob) GetAppendClientIdentity() bool { return false }
|
||||||
|
func (j *PullJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
||||||
|
|
||||||
type PositiveDurationOrManual struct {
|
type PositiveDurationOrManual struct {
|
||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
Manual bool
|
Manual bool
|
||||||
@@ -120,15 +184,24 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
|
|||||||
|
|
||||||
type SinkJob struct {
|
type SinkJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
|
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *SinkJob) GetRootFS() string { return j.RootFS }
|
||||||
|
func (j *SinkJob) GetAppendClientIdentity() bool { return true }
|
||||||
|
func (j *SinkJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
||||||
|
|
||||||
type SourceJob struct {
|
type SourceJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *SourceJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
||||||
|
func (j *SourceJob) GetSendOptions() *SendOptions { return j.Send }
|
||||||
|
|
||||||
type FilesystemsFilter map[string]bool
|
type FilesystemsFilter map[string]bool
|
||||||
|
|
||||||
type SnapshottingEnum struct {
|
type SnapshottingEnum struct {
|
||||||
@@ -245,14 +318,16 @@ type ServeCommon struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TCPServe struct {
|
type TCPServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
Clients map[string]string `yaml:"clients"`
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
|
Clients map[string]string `yaml:"clients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSServe struct {
|
type TLSServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
Ca string `yaml:"ca"`
|
Ca string `yaml:"ca"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
@@ -282,6 +357,7 @@ type PruneKeepNotReplicated struct {
|
|||||||
type PruneKeepLastN struct {
|
type PruneKeepLastN struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Count int `yaml:"count"`
|
Count int `yaml:"count"`
|
||||||
|
Regex string `yaml:"regex,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
||||||
@@ -331,8 +407,9 @@ type MonitoringEnum struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrometheusMonitoring struct {
|
type PrometheusMonitoring struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyslogFacility syslog.Priority
|
type SyslogFacility syslog.Priority
|
||||||
@@ -589,7 +666,7 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
|
|||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||||
|
|
||||||
func parsePostitiveDuration(e string) (d time.Duration, err error) {
|
func parsePositiveDuration(e string) (d time.Duration, err error) {
|
||||||
comps := durationStringRegex.FindStringSubmatch(e)
|
comps := durationStringRegex.FindStringSubmatch(e)
|
||||||
if len(comps) != 3 {
|
if len(comps) != 3 {
|
||||||
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type A struct {
|
||||||
|
B *B `yaml:"b,optional,fromdefaults"`
|
||||||
|
A1 string `yaml:"a1,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type B struct {
|
||||||
|
C *C `yaml:"c,optional,fromdefaults"`
|
||||||
|
D string `yaml:"d,default=ddd"`
|
||||||
|
E string `yaml:"e,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type C struct {
|
||||||
|
Q string `yaml:"q,optional"`
|
||||||
|
R string `yaml:"r,default=r"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDepFromDefaults(t *testing.T) {
|
||||||
|
|
||||||
|
type testcase struct {
|
||||||
|
name string
|
||||||
|
yaml string
|
||||||
|
expect *A
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []testcase{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
yaml: `{}`,
|
||||||
|
expect: &A{
|
||||||
|
B: &B{
|
||||||
|
C: &C{
|
||||||
|
R: "r",
|
||||||
|
},
|
||||||
|
D: "ddd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a1 set",
|
||||||
|
yaml: `{"a1":"blah"}`,
|
||||||
|
expect: &A{
|
||||||
|
A1: "blah",
|
||||||
|
B: &B{
|
||||||
|
C: &C{
|
||||||
|
R: "r",
|
||||||
|
},
|
||||||
|
D: "ddd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "D set",
|
||||||
|
yaml: `
|
||||||
|
b:
|
||||||
|
d: 4d
|
||||||
|
`,
|
||||||
|
expect: &A{
|
||||||
|
B: &B{
|
||||||
|
D: "4d",
|
||||||
|
C: &C{
|
||||||
|
R: "r",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tci := range tcs {
|
||||||
|
t.Run(fmt.Sprintf("%d-%s", tci, tcs[tci].name), func(t *testing.T) {
|
||||||
|
tc := tcs[tci]
|
||||||
|
|
||||||
|
var a A
|
||||||
|
err := yaml.UnmarshalStrict([]byte(tc.yaml), &a)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expect, &a)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
clients: {
|
clients: {
|
||||||
"10.0.0.1":"foo"
|
"10.0.0.1":"foo"
|
||||||
}
|
}
|
||||||
root_fs: zoot/foo
|
root_fs: zroot/foo
|
||||||
`
|
`
|
||||||
_, err := ParseConfigBytes([]byte(jobdef))
|
_, err := ParseConfigBytes([]byte(jobdef))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -70,9 +70,9 @@ func TestPrometheusMonitoring(t *testing.T) {
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9091'
|
listen: ':9811'
|
||||||
`)
|
`)
|
||||||
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
assert.Equal(t, ":9811", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package config
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecvOptions(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: foo
|
||||||
|
type: pull
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: foo
|
||||||
|
client_identity: bar
|
||||||
|
root_fs: "zreplplatformtest"
|
||||||
|
%s
|
||||||
|
interval: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_properties_empty := `
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_inherit_empty := `
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
inherit:
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_inherit := `
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
inherit:
|
||||||
|
- testprop
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_override_empty := `
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
override:
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_override := `
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
override:
|
||||||
|
testprop2: "test123"
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_override_and_inherit := `
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
inherit:
|
||||||
|
- testprop
|
||||||
|
override:
|
||||||
|
testprop2: "test123"
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_empty := `
|
||||||
|
recv: {}
|
||||||
|
`
|
||||||
|
|
||||||
|
recv_not_specified := `
|
||||||
|
`
|
||||||
|
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
t.Run("recv_inherit_empty", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_inherit_empty))
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recv_inherit", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_inherit))
|
||||||
|
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
||||||
|
assert.NotEmpty(t, inherit)
|
||||||
|
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recv_override_empty", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_override_empty))
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recv_override", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_override))
|
||||||
|
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
||||||
|
require.Len(t, override, 1)
|
||||||
|
require.Equal(t, "test123", override["testprop2"])
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recv_override_and_inherit", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_override_and_inherit))
|
||||||
|
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
||||||
|
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
||||||
|
assert.NotEmpty(t, inherit)
|
||||||
|
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
||||||
|
assert.NotEmpty(t, override)
|
||||||
|
assert.Equal(t, "test123", override["testprop2"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recv_properties_empty", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_properties_empty))
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recv_empty", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_empty))
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(recv_not_specified))
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSendOptions(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: foo
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: foo
|
||||||
|
client_identity: bar
|
||||||
|
filesystems: {"<": true}
|
||||||
|
%s
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
encrypted_false := `
|
||||||
|
send:
|
||||||
|
encrypted: false
|
||||||
|
`
|
||||||
|
|
||||||
|
encrypted_true := `
|
||||||
|
send:
|
||||||
|
encrypted: true
|
||||||
|
`
|
||||||
|
|
||||||
|
raw_true := `
|
||||||
|
send:
|
||||||
|
raw: true
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
raw_false := `
|
||||||
|
send:
|
||||||
|
raw: false
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
raw_and_encrypted := `
|
||||||
|
send:
|
||||||
|
encrypted: true
|
||||||
|
raw: true
|
||||||
|
`
|
||||||
|
|
||||||
|
properties_and_encrypted := `
|
||||||
|
send:
|
||||||
|
encrypted: true
|
||||||
|
send_properties: true
|
||||||
|
`
|
||||||
|
|
||||||
|
properties_true := `
|
||||||
|
send:
|
||||||
|
send_properties: true
|
||||||
|
`
|
||||||
|
|
||||||
|
properties_false := `
|
||||||
|
send:
|
||||||
|
send_properties: false
|
||||||
|
`
|
||||||
|
|
||||||
|
send_empty := `
|
||||||
|
send: {}
|
||||||
|
`
|
||||||
|
|
||||||
|
send_not_specified := `
|
||||||
|
`
|
||||||
|
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
t.Run("encrypted_false", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(encrypted_false))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, false, encrypted)
|
||||||
|
})
|
||||||
|
t.Run("encrypted_true", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(encrypted_true))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, true, encrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send_empty", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(send_empty))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, false, encrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("properties_and_encrypted", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(properties_and_encrypted))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
properties := c.Jobs[0].Ret.(*PushJob).Send.SendProperties
|
||||||
|
assert.Equal(t, true, encrypted)
|
||||||
|
assert.Equal(t, true, properties)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("properties_false", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(properties_false))
|
||||||
|
properties := c.Jobs[0].Ret.(*PushJob).Send.SendProperties
|
||||||
|
assert.Equal(t, false, properties)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("properties_true", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(properties_true))
|
||||||
|
properties := c.Jobs[0].Ret.(*PushJob).Send.SendProperties
|
||||||
|
assert.Equal(t, true, properties)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("raw_true", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(raw_true))
|
||||||
|
raw := c.Jobs[0].Ret.(*PushJob).Send.Raw
|
||||||
|
assert.Equal(t, true, raw)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("raw_false", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(raw_false))
|
||||||
|
raw := c.Jobs[0].Ret.(*PushJob).Send.Raw
|
||||||
|
assert.Equal(t, false, raw)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("raw_and_encrypted", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(raw_and_encrypted))
|
||||||
|
raw := c.Jobs[0].Ret.(*PushJob).Send.Raw
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, true, raw)
|
||||||
|
assert.Equal(t, true, encrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
|
c := testValidConfig(t, fill(send_not_specified))
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// template must be a template/text template with a single '{{ . }}' as placehodler for val
|
// template must be a template/text template with a single '{{ . }}' as placeholder for val
|
||||||
//nolint[:deadcode,unused]
|
//nolint[:deadcode,unused]
|
||||||
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
||||||
tmp, err := template.New("master").Parse(tmpl)
|
tmp, err := template.New("master").Parse(tmpl)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
intervals, err := parseRetentionGridIntervalsString(in)
|
intervals, err := ParseRetentionIntervalSpec(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
|
|||||||
return nil, fmt.Errorf("contains factor <= 0")
|
return nil, fmt.Errorf("contains factor <= 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, err := parsePostitiveDuration(comps[2])
|
duration, err := parsePositiveDuration(comps[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRetentionGridIntervalsString(s string) (intervals []RetentionInterval, err error) {
|
func ParseRetentionIntervalSpec(s string) (intervals []RetentionInterval, err error) {
|
||||||
|
|
||||||
ges := strings.Split(s, "|")
|
ges := strings.Split(s, "|")
|
||||||
intervals = make([]RetentionInterval, 0, 7*len(ges))
|
intervals = make([]RetentionInterval, 0, 7*len(ges))
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: sink
|
||||||
|
name: "limited_sink"
|
||||||
|
root_fs: "fs0"
|
||||||
|
recv:
|
||||||
|
bandwidth_limit:
|
||||||
|
max: 12345 B
|
||||||
|
serve:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
|
|
||||||
|
- type: push
|
||||||
|
name: "limited_push"
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
|
client_identity: local_backup
|
||||||
|
filesystems: {
|
||||||
|
"root<": true,
|
||||||
|
}
|
||||||
|
send:
|
||||||
|
bandwidth_limit:
|
||||||
|
max: 54321 B
|
||||||
|
bucket_capacity: 1024 B
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 1
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 1
|
||||||
|
|
||||||
|
- type: sink
|
||||||
|
name: "nolimit_sink"
|
||||||
|
root_fs: "fs1"
|
||||||
|
serve:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
@@ -10,6 +10,8 @@ jobs:
|
|||||||
address: "backup-server.foo.bar:8888"
|
address: "backup-server.foo.bar:8888"
|
||||||
snapshotting:
|
snapshotting:
|
||||||
type: manual
|
type: manual
|
||||||
|
send:
|
||||||
|
encrypted: false
|
||||||
pruning:
|
pruning:
|
||||||
keep_sender:
|
keep_sender:
|
||||||
- type: not_replicated
|
- type: not_replicated
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# This config serves as an example for a local zrepl installation that
|
||||||
|
# backups the entire zpool `system` to `backuppool/zrepl/sink`
|
||||||
|
#
|
||||||
|
# The requirements covered by this setup are described in the zrepl documentation's
|
||||||
|
# quick start section which inlines this example.
|
||||||
|
#
|
||||||
|
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
||||||
|
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_drive`
|
||||||
|
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
||||||
|
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
||||||
|
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
# this job takes care of snapshot creation + pruning
|
||||||
|
- name: snapjob
|
||||||
|
type: snap
|
||||||
|
filesystems: {
|
||||||
|
"system<": true,
|
||||||
|
}
|
||||||
|
# create snapshots with prefix `zrepl_` every 15 minutes
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
interval: 15m
|
||||||
|
prefix: zrepl_
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
# fade-out scheme for snapshots starting with `zrepl_`
|
||||||
|
# - keep all created in the last hour
|
||||||
|
# - then destroy snapshots such that we keep 24 each 1 hour apart
|
||||||
|
# - then destroy snapshots such that we keep 14 each 1 day apart
|
||||||
|
# - then destroy all older snapshots
|
||||||
|
- type: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 14x1d
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
# keep all snapshots that don't have the `zrepl_` prefix
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
|
||||||
|
# This job pushes to the local sink defined in job `backuppool_sink`.
|
||||||
|
# We trigger replication manually from the command line / udev rules using
|
||||||
|
# `zrepl signal wakeup push_to_drive`
|
||||||
|
- type: push
|
||||||
|
name: push_to_drive
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: backuppool_sink
|
||||||
|
client_identity: myhostname
|
||||||
|
filesystems: {
|
||||||
|
"system<": true
|
||||||
|
}
|
||||||
|
send:
|
||||||
|
encrypted: true
|
||||||
|
replication:
|
||||||
|
protection:
|
||||||
|
initial: guarantee_resumability
|
||||||
|
# Downgrade protection to guarantee_incremental which uses zfs bookmarks instead of zfs holds.
|
||||||
|
# Thus, when we yank out the backup drive during replication
|
||||||
|
# - we might not be able to resume the interrupted replication step because the partially received `to` snapshot of a `from`->`to` step may be pruned any time
|
||||||
|
# - but in exchange we get back the disk space allocated by `to` when we prune it
|
||||||
|
# - and because we still have the bookmarks created by `guarantee_incremental`, we can still do incremental replication of `from`->`to2` in the future
|
||||||
|
incremental: guarantee_incremental
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
# no-op prune rule on sender (keep all snapshots), job `snapshot` takes care of this
|
||||||
|
keep_sender:
|
||||||
|
- type: regex
|
||||||
|
regex: ".*"
|
||||||
|
# retain
|
||||||
|
keep_receiver:
|
||||||
|
# longer retention on the backup drive, we have more space there
|
||||||
|
- type: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 360x1d
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
# retain all non-zrepl snapshots on the backup drive
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
|
||||||
|
# This job receives from job `push_to_drive` into `backuppool/zrepl/sink/myhostname`
|
||||||
|
- type: sink
|
||||||
|
name: backuppool_sink
|
||||||
|
root_fs: "backuppool/zrepl/sink"
|
||||||
|
serve:
|
||||||
|
type: local
|
||||||
|
listener_name: backuppool_sink
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
jobs:
|
||||||
|
- name: sink
|
||||||
|
type: sink
|
||||||
|
serve:
|
||||||
|
type: tls
|
||||||
|
listen: ":8888"
|
||||||
|
ca: "/etc/zrepl/prod.crt"
|
||||||
|
cert: "/etc/zrepl/backups.crt"
|
||||||
|
key: "/etc/zrepl/backups.key"
|
||||||
|
client_cns:
|
||||||
|
- "prod"
|
||||||
|
root_fs: "storage/zrepl/sink"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
jobs:
|
||||||
|
- name: prod_to_backups
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
type: tls
|
||||||
|
address: "backups.example.com:8888"
|
||||||
|
ca: /etc/zrepl/backups.crt
|
||||||
|
cert: /etc/zrepl/prod.crt
|
||||||
|
key: /etc/zrepl/prod.key
|
||||||
|
server_cn: "backups"
|
||||||
|
filesystems: {
|
||||||
|
"zroot/var/db": true,
|
||||||
|
"zroot/usr/home<": true,
|
||||||
|
"zroot/usr/home/paranoid": false
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
interval: 10m
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: not_replicated
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
|
||||||
|
regex: "^zrepl_"
|
||||||
@@ -5,7 +5,11 @@ jobs:
|
|||||||
type: tcp
|
type: tcp
|
||||||
listen: "0.0.0.0:8888"
|
listen: "0.0.0.0:8888"
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "client1"
|
"192.168.122.123" : "mysql01",
|
||||||
|
"192.168.122.42" : "mx01",
|
||||||
|
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||||
|
"10.23.42.0/24": "cluster-*",
|
||||||
|
"fde4:8dba:82e1::/64": "san-*",
|
||||||
}
|
}
|
||||||
filesystems: {
|
filesystems: {
|
||||||
"<": true,
|
"<": true,
|
||||||
|
|||||||
+15
-3
@@ -15,10 +15,12 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/nethelpers"
|
"github.com/zrepl/zrepl/daemon/nethelpers"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
type controlJob struct {
|
type controlJob struct {
|
||||||
@@ -44,6 +46,8 @@ func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInt
|
|||||||
|
|
||||||
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||||
|
|
||||||
|
func (j *controlJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
var promControl struct {
|
var promControl struct {
|
||||||
requestBegin *prometheus.CounterVec
|
requestBegin *prometheus.CounterVec
|
||||||
requestFinished *prometheus.HistogramVec
|
requestFinished *prometheus.HistogramVec
|
||||||
@@ -61,7 +65,7 @@ func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "control",
|
Subsystem: "control",
|
||||||
Name: "request_finished",
|
Name: "request_finished",
|
||||||
Help: "time it took a request to finih",
|
Help: "time it took a request to finish",
|
||||||
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
|
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
|
||||||
}, []string{"endpoint"})
|
}, []string{"endpoint"})
|
||||||
registerer.MustRegister(promControl.requestBegin)
|
registerer.MustRegister(promControl.requestBegin)
|
||||||
@@ -114,7 +118,15 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
mux.Handle(ControlJobEndpointStatus,
|
mux.Handle(ControlJobEndpointStatus,
|
||||||
// don't log requests to status endpoint, too spammy
|
// don't log requests to status endpoint, too spammy
|
||||||
jsonResponder{log, func() (interface{}, error) {
|
jsonResponder{log, func() (interface{}, error) {
|
||||||
s := j.jobs.status()
|
jobs := j.jobs.status()
|
||||||
|
globalZFS := zfscmd.GetReport()
|
||||||
|
envconstReport := envconst.GetReport()
|
||||||
|
s := Status{
|
||||||
|
Jobs: jobs,
|
||||||
|
Global: GlobalStatus{
|
||||||
|
ZFSCmds: globalZFS,
|
||||||
|
Envconst: envconstReport,
|
||||||
|
}}
|
||||||
return s, nil
|
return s, nil
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -247,7 +259,7 @@ func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(res)
|
encodeErr := json.NewEncoder(&buf).Encode(res)
|
||||||
if encodeErr != nil {
|
if encodeErr != nil {
|
||||||
j.log.WithError(producerErr).Error("control handler json marhsal error")
|
j.log.WithError(producerErr).Error("control handler json marshal error")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, err := io.WriteString(w, encodeErr.Error())
|
_, err := io.WriteString(w, encodeErr.Error())
|
||||||
logIoErr(err)
|
logIoErr(err)
|
||||||
|
|||||||
+51
-16
@@ -3,6 +3,7 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,6 +14,10 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||||
@@ -20,11 +25,11 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
|
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(conf *config.Config) error {
|
func Run(ctx context.Context, conf *config.Config) error {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
@@ -34,10 +39,17 @@ func Run(conf *config.Config) error {
|
|||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// The math/rand package is used presently for generating trace IDs, we
|
||||||
|
// seed it with the current time and pid so that the IDs are mostly
|
||||||
|
// unique.
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
rand.Seed(int64(os.Getpid()))
|
||||||
|
|
||||||
outlets, err := logging.OutletsFromConfig(*conf.Global.Logging)
|
outlets, err := logging.OutletsFromConfig(*conf.Global.Logging)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot build logging from config")
|
return errors.Wrap(err, "cannot build logging from config")
|
||||||
}
|
}
|
||||||
|
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
|
||||||
|
|
||||||
confJobs, err := job.JobsFromConfig(conf)
|
confJobs, err := job.JobsFromConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,14 +59,23 @@ func Run(conf *config.Config) error {
|
|||||||
log := logger.NewLogger(outlets, 1*time.Second)
|
log := logger.NewLogger(outlets, 1*time.Second)
|
||||||
log.Info(version.NewZreplVersionInformation().String())
|
log.Info(version.NewZreplVersionInformation().String())
|
||||||
|
|
||||||
|
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
|
||||||
|
trace.RegisterCallback(trace.Callback{
|
||||||
|
OnBegin: func(ctx context.Context) { logging.GetLogger(ctx, logging.SubsysTraceData).Debug("begin span") },
|
||||||
|
OnEnd: func(ctx context.Context, spanInfo trace.SpanInfo) {
|
||||||
|
logging.
|
||||||
|
GetLogger(ctx, logging.SubsysTraceData).
|
||||||
|
WithField("duration_s", spanInfo.EndedAt().Sub(spanInfo.StartedAt()).Seconds()).
|
||||||
|
Debug("finished span " + spanInfo.TaskAndSpanStack(trace.SpanStackKindAnnotation))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
for _, job := range confJobs {
|
for _, job := range confJobs {
|
||||||
if IsInternalJobName(job.Name()) {
|
if IsInternalJobName(job.Name()) {
|
||||||
panic(fmt.Sprintf("internal job name used for config job '%s'", job.Name())) //FIXME
|
panic(fmt.Sprintf("internal job name used for config job '%s'", job.Name())) //FIXME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = job.WithLogger(ctx, log)
|
|
||||||
|
|
||||||
jobs := newJobs()
|
jobs := newJobs()
|
||||||
|
|
||||||
// start control socket
|
// start control socket
|
||||||
@@ -76,11 +97,17 @@ func Run(conf *config.Config) error {
|
|||||||
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
|
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "cannot build monitorin gjob #%d", i)
|
return errors.Wrapf(err, "cannot build monitoring job #%d", i)
|
||||||
}
|
}
|
||||||
jobs.start(ctx, job, true)
|
jobs.start(ctx, job, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register global (=non job-local) metrics
|
||||||
|
version.PrometheusRegister(prometheus.DefaultRegisterer)
|
||||||
|
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||||
|
trace.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||||
|
endpoint.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||||
|
|
||||||
log.Info("starting daemon")
|
log.Info("starting daemon")
|
||||||
|
|
||||||
// start regular jobs
|
// start regular jobs
|
||||||
@@ -94,6 +121,8 @@ func Run(conf *config.Config) error {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.WithError(ctx.Err()).Info("context finished")
|
log.WithError(ctx.Err()).Info("context finished")
|
||||||
}
|
}
|
||||||
|
log.Info("waiting for jobs to finish")
|
||||||
|
<-jobs.wait()
|
||||||
log.Info("daemon exiting")
|
log.Info("daemon exiting")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -116,18 +145,25 @@ func newJobs() *jobs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
logJobField string = "job"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *jobs) wait() <-chan struct{} {
|
func (s *jobs) wait() <-chan struct{} {
|
||||||
ch := make(chan struct{})
|
ch := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
|
close(ch)
|
||||||
}()
|
}()
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
Jobs map[string]*job.Status
|
||||||
|
Global GlobalStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalStatus struct {
|
||||||
|
ZFSCmds *zfscmd.Report
|
||||||
|
Envconst *envconst.Report
|
||||||
|
}
|
||||||
|
|
||||||
func (s *jobs) status() map[string]*job.Status {
|
func (s *jobs) status() map[string]*job.Status {
|
||||||
s.m.RLock()
|
s.m.RLock()
|
||||||
defer s.m.RUnlock()
|
defer s.m.RUnlock()
|
||||||
@@ -189,9 +225,8 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
s.m.Lock()
|
s.m.Lock()
|
||||||
defer s.m.Unlock()
|
defer s.m.Unlock()
|
||||||
|
|
||||||
jobLog := job.GetLogger(ctx).
|
ctx = logging.WithInjectedField(ctx, logging.JobField, j.Name())
|
||||||
WithField(logJobField, j.Name()).
|
|
||||||
WithOutlet(newPrometheusLogOutlet(j.Name()), logger.Debug)
|
|
||||||
jobName := j.Name()
|
jobName := j.Name()
|
||||||
if !internal && IsInternalJobName(jobName) {
|
if !internal && IsInternalJobName(jobName) {
|
||||||
panic(fmt.Sprintf("internal job name used for non-internal job %s", jobName))
|
panic(fmt.Sprintf("internal job name used for non-internal job %s", jobName))
|
||||||
@@ -206,7 +241,7 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
j.RegisterMetrics(prometheus.DefaultRegisterer)
|
j.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||||
|
|
||||||
s.jobs[jobName] = j
|
s.jobs[jobName] = j
|
||||||
ctx = job.WithLogger(ctx, jobLog)
|
ctx = zfscmd.WithJobID(ctx, j.Name())
|
||||||
ctx, wakeup := wakeup.Context(ctx)
|
ctx, wakeup := wakeup.Context(ctx)
|
||||||
ctx, resetFunc := reset.Context(ctx)
|
ctx, resetFunc := reset.Context(ctx)
|
||||||
s.wakeups[jobName] = wakeup
|
s.wakeups[jobName] = wakeup
|
||||||
@@ -215,8 +250,8 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer s.wg.Done()
|
defer s.wg.Done()
|
||||||
jobLog.Info("starting job")
|
job.GetLogger(ctx).Info("starting job")
|
||||||
defer jobLog.Info("job exited")
|
defer job.GetLogger(ctx).Info("job exited")
|
||||||
j.Run(ctx)
|
j.Run(ctx)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func (m DatasetMapFilter) Filter(p *zfs.DatasetPath) (pass bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct a new filter-only DatasetMapFilter from a mapping
|
// Construct a new filter-only DatasetMapFilter from a mapping
|
||||||
// The new filter allows excactly those paths that were not forbidden by the mapping.
|
// The new filter allows exactly those paths that were not forbidden by the mapping.
|
||||||
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
||||||
|
|
||||||
if m.filterMode {
|
if m.filterMode {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package filters
|
package filters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestDatasetMapFilter(t *testing.T) {
|
func TestDatasetMapFilter(t *testing.T) {
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package filters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AnyFSVFilter struct{}
|
|
||||||
|
|
||||||
func NewAnyFSVFilter() AnyFSVFilter {
|
|
||||||
return AnyFSVFilter{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ zfs.FilesystemVersionFilter = AnyFSVFilter{}
|
|
||||||
|
|
||||||
func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrefixFilter struct {
|
|
||||||
prefix string
|
|
||||||
fstype zfs.VersionType
|
|
||||||
fstypeSet bool // optionals anyone?
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ zfs.FilesystemVersionFilter = &PrefixFilter{}
|
|
||||||
|
|
||||||
func NewPrefixFilter(prefix string) *PrefixFilter {
|
|
||||||
return &PrefixFilter{prefix: prefix}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTypedPrefixFilter(prefix string, versionType zfs.VersionType) *PrefixFilter {
|
|
||||||
return &PrefixFilter{prefix, versionType, true}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *PrefixFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
|
||||||
fstypeMatches := (!f.fstypeSet || t == f.fstype)
|
|
||||||
prefixMatches := strings.HasPrefix(name, f.prefix)
|
|
||||||
return fstypeMatches && prefixMatches, nil
|
|
||||||
}
|
|
||||||
@@ -6,29 +6,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyLog contextKey = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
type Logger = logger.Logger
|
||||||
|
|
||||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyLog, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogger(ctx context.Context) Logger { return getLogger(ctx) }
|
func GetLogger(ctx context.Context) Logger { return getLogger(ctx) }
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
func getLogger(ctx context.Context) Logger {
|
||||||
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
return logging.GetLogger(ctx, logging.SubsysHooks)
|
||||||
return log
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
@@ -69,6 +72,9 @@ func curry(f comparisonAssertionFunc, expected interface{}, right bool) (ret val
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHooks(t *testing.T) {
|
func TestHooks(t *testing.T) {
|
||||||
|
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||||
|
defer end()
|
||||||
|
|
||||||
testFSName := "testpool/testdataset"
|
testFSName := "testpool/testdataset"
|
||||||
testSnapshotName := "testsnap"
|
testSnapshotName := "testsnap"
|
||||||
|
|
||||||
@@ -418,9 +424,8 @@ jobs:
|
|||||||
|
|
||||||
cbReached = false
|
cbReached = false
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
if testing.Verbose() && !tt.SuppressOutput {
|
if testing.Verbose() && !tt.SuppressOutput {
|
||||||
ctx = hooks.WithLogger(ctx, log)
|
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
|
||||||
}
|
}
|
||||||
plan.Run(ctx, false)
|
plan.Run(ctx, false)
|
||||||
report := plan.Report()
|
report := plan.Report()
|
||||||
|
|||||||
+171
-60
@@ -2,17 +2,20 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/common/log"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -28,14 +31,17 @@ import (
|
|||||||
|
|
||||||
type ActiveSide struct {
|
type ActiveSide struct {
|
||||||
mode activeMode
|
mode activeMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
connecter transport.Connecter
|
connecter transport.Connecter
|
||||||
|
|
||||||
|
replicationDriverConfig driver.Config
|
||||||
|
|
||||||
prunerFactory *pruner.PrunerFactory
|
prunerFactory *pruner.PrunerFactory
|
||||||
|
|
||||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
promRepStateSecs *prometheus.HistogramVec // labels: state
|
||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||||
|
promReplicationErrors prometheus.Gauge
|
||||||
|
|
||||||
tasksMtx sync.Mutex
|
tasksMtx sync.Mutex
|
||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
@@ -78,31 +84,33 @@ func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type activeMode interface {
|
type activeMode interface {
|
||||||
ConnectEndpoints(rpcLoggers rpc.Loggers, connecter transport.Connecter)
|
ConnectEndpoints(ctx context.Context, connecter transport.Connecter)
|
||||||
DisconnectEndpoints()
|
DisconnectEndpoints()
|
||||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||||
Type() Type
|
Type() Type
|
||||||
|
PlannerPolicy() logic.PlannerPolicy
|
||||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
||||||
SnapperReport() *snapper.Report
|
SnapperReport() *snapper.Report
|
||||||
ResetConnectBackoff()
|
ResetConnectBackoff()
|
||||||
}
|
}
|
||||||
|
|
||||||
type modePush struct {
|
type modePush struct {
|
||||||
setupMtx sync.Mutex
|
setupMtx sync.Mutex
|
||||||
sender *endpoint.Sender
|
sender *endpoint.Sender
|
||||||
receiver *rpc.Client
|
receiver *rpc.Client
|
||||||
fsfilter endpoint.FSFilter
|
senderConfig *endpoint.SenderConfig
|
||||||
snapper *snapper.PeriodicOrManual
|
plannerPolicy *logic.PlannerPolicy
|
||||||
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
||||||
m.setupMtx.Lock()
|
m.setupMtx.Lock()
|
||||||
defer m.setupMtx.Unlock()
|
defer m.setupMtx.Unlock()
|
||||||
if m.receiver != nil || m.sender != nil {
|
if m.receiver != nil || m.sender != nil {
|
||||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||||
}
|
}
|
||||||
m.sender = endpoint.NewSender(m.fsfilter)
|
m.sender = endpoint.NewSender(*m.senderConfig)
|
||||||
m.receiver = rpc.NewClient(connecter, loggers)
|
m.receiver = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) DisconnectEndpoints() {
|
func (m *modePush) DisconnectEndpoints() {
|
||||||
@@ -121,6 +129,8 @@ func (m *modePush) SenderReceiver() (logic.Sender, logic.Receiver) {
|
|||||||
|
|
||||||
func (m *modePush) Type() Type { return TypePush }
|
func (m *modePush) Type() Type { return TypePush }
|
||||||
|
|
||||||
|
func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
m.snapper.Run(ctx, wakeUpCommon)
|
m.snapper.Run(ctx, wakeUpCommon)
|
||||||
}
|
}
|
||||||
@@ -137,15 +147,30 @@ func (m *modePush) ResetConnectBackoff() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error) {
|
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||||
m := &modePush{}
|
m := &modePush{}
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
var err error
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
|
||||||
}
|
|
||||||
m.fsfilter = fsf
|
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "sender config")
|
||||||
|
}
|
||||||
|
|
||||||
|
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "field `replication`")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
|
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||||
|
ReplicationConfig: replicationConfig,
|
||||||
|
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||||
|
}
|
||||||
|
if err := m.plannerPolicy.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build planner policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,21 +178,22 @@ func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modePull struct {
|
type modePull struct {
|
||||||
setupMtx sync.Mutex
|
setupMtx sync.Mutex
|
||||||
receiver *endpoint.Receiver
|
receiver *endpoint.Receiver
|
||||||
sender *rpc.Client
|
receiverConfig endpoint.ReceiverConfig
|
||||||
rootFS *zfs.DatasetPath
|
sender *rpc.Client
|
||||||
interval config.PositiveDurationOrManual
|
plannerPolicy *logic.PlannerPolicy
|
||||||
|
interval config.PositiveDurationOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
func (m *modePull) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
||||||
m.setupMtx.Lock()
|
m.setupMtx.Lock()
|
||||||
defer m.setupMtx.Unlock()
|
defer m.setupMtx.Unlock()
|
||||||
if m.receiver != nil || m.sender != nil {
|
if m.receiver != nil || m.sender != nil {
|
||||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||||
}
|
}
|
||||||
m.receiver = endpoint.NewReceiver(m.rootFS, false)
|
m.receiver = endpoint.NewReceiver(m.receiverConfig)
|
||||||
m.sender = rpc.NewClient(connecter, loggers)
|
m.sender = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePull) DisconnectEndpoints() {
|
func (m *modePull) DisconnectEndpoints() {
|
||||||
@@ -186,6 +212,8 @@ func (m *modePull) SenderReceiver() (logic.Sender, logic.Receiver) {
|
|||||||
|
|
||||||
func (*modePull) Type() Type { return TypePull }
|
func (*modePull) Type() Type { return TypePull }
|
||||||
|
|
||||||
|
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
if m.interval.Manual {
|
if m.interval.Manual {
|
||||||
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
||||||
@@ -223,40 +251,85 @@ func (m *modePull) ResetConnectBackoff() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err error) {
|
func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.JobID) (m *modePull, err error) {
|
||||||
m = &modePull{}
|
m = &modePull{}
|
||||||
m.interval = in.Interval
|
m.interval = in.Interval
|
||||||
|
|
||||||
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("RootFS is not a valid zfs filesystem path")
|
return nil, errors.Wrap(err, "field `replication`")
|
||||||
}
|
}
|
||||||
if m.rootFS.Length() <= 0 {
|
|
||||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
|
EncryptedSend: logic.DontCare,
|
||||||
|
ReplicationConfig: replicationConfig,
|
||||||
|
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||||
|
}
|
||||||
|
if err := m.plannerPolicy.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build planner policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *ActiveSide, err error) {
|
func replicationDriverConfigFromConfig(in *config.Replication) (c driver.Config, err error) {
|
||||||
|
c = driver.Config{
|
||||||
|
StepQueueConcurrency: in.Concurrency.Steps,
|
||||||
|
MaxAttempts: envconst.Int("ZREPL_REPLICATION_MAX_ATTEMPTS", 3),
|
||||||
|
ReconnectHardFailTimeout: envconst.Duration("ZREPL_REPLICATION_RECONNECT_HARD_FAIL_TIMEOUT", 10*time.Minute),
|
||||||
|
}
|
||||||
|
err = c.Validate()
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||||
|
|
||||||
|
j = &ActiveSide{}
|
||||||
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := configJob.(type) {
|
||||||
|
case *config.PushJob:
|
||||||
|
j.mode, err = modePushFromConfig(g, v, j.name) // shadow
|
||||||
|
case *config.PullJob:
|
||||||
|
j.mode, err = modePullFromConfig(g, v, j.name) // shadow
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("implementation error: unknown job type %T", v))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // no wrapping required
|
||||||
|
}
|
||||||
|
|
||||||
j = &ActiveSide{mode: mode}
|
|
||||||
j.name = in.Name
|
|
||||||
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
Name: "state_time",
|
Name: "state_time",
|
||||||
Help: "seconds spent during replication",
|
Help: "seconds spent during replication",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"state"})
|
}, []string{"state"})
|
||||||
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
Name: "bytes_replicated",
|
Name: "bytes_replicated",
|
||||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"filesystem"})
|
}, []string{"filesystem"})
|
||||||
|
|
||||||
|
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "zrepl",
|
||||||
|
Subsystem: "replication",
|
||||||
|
Name: "filesystem_errors",
|
||||||
|
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
||||||
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
|
})
|
||||||
|
|
||||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build client")
|
return nil, errors.Wrap(err, "cannot build client")
|
||||||
@@ -267,13 +340,18 @@ func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *Act
|
|||||||
Subsystem: "pruning",
|
Subsystem: "pruning",
|
||||||
Name: "time",
|
Name: "time",
|
||||||
Help: "seconds spent in pruner",
|
Help: "seconds spent in pruner",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"prune_side"})
|
}, []string{"prune_side"})
|
||||||
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
j.replicationDriverConfig, err = replicationDriverConfigFromConfig(in.Replication)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build replication driver config")
|
||||||
|
}
|
||||||
|
|
||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,9 +359,10 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promRepStateSecs)
|
registerer.MustRegister(j.promRepStateSecs)
|
||||||
registerer.MustRegister(j.promPruneSecs)
|
registerer.MustRegister(j.promPruneSecs)
|
||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
|
registerer.MustRegister(j.promReplicationErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name }
|
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||||
|
|
||||||
type ActiveSideStatus struct {
|
type ActiveSideStatus struct {
|
||||||
Replication *report.Report
|
Replication *report.Report
|
||||||
@@ -315,19 +394,43 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|||||||
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return pull.rootFS.Copy(), true
|
return pull.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
|
push, ok := j.mode.(*modePush)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modePull) // make sure we didn't introduce a new job type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return push.senderConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// The active side of a replication uses one end (sender or receiver)
|
||||||
|
// directly by method invocation, without going through a transport that
|
||||||
|
// provides a client identity.
|
||||||
|
// However, in order to avoid the need to distinguish between direct-method-invocating
|
||||||
|
// clients and RPC client, we use an invalid client identity as a sentinel value.
|
||||||
|
func FakeActiveSideDirectMethodInvocationClientIdentity(jobId endpoint.JobID) string {
|
||||||
|
return fmt.Sprintf("<local><active><job><client><identity><job=%q>", jobId.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Run(ctx context.Context) {
|
func (j *ActiveSide) Run(ctx context.Context) {
|
||||||
|
ctx, endTask := trace.WithTaskAndSpan(ctx, "active-side-job", j.Name())
|
||||||
|
defer endTask()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, endpoint.ClientIdentityKey, FakeActiveSideDirectMethodInvocationClientIdentity(j.name))
|
||||||
|
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
|
||||||
|
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
|
||||||
periodicDone := make(chan struct{})
|
periodicDone := make(chan struct{})
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go j.mode.RunPeriodic(ctx, periodicDone)
|
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
|
||||||
|
defer endTask()
|
||||||
|
go j.mode.RunPeriodic(periodicCtx, periodicDone)
|
||||||
|
|
||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
@@ -343,17 +446,15 @@ outer:
|
|||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
invLog := log.WithField("invocation", invocationCount)
|
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
||||||
j.do(WithLogger(ctx, invLog))
|
j.do(invocationCtx)
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) do(ctx context.Context) {
|
func (j *ActiveSide) do(ctx context.Context) {
|
||||||
|
|
||||||
log := GetLogger(ctx)
|
j.mode.ConnectEndpoints(ctx, j.connecter)
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
|
||||||
loggers := rpc.GetLoggersOrPanic(ctx) // filled by WithSubsystemLoggers
|
|
||||||
j.mode.ConnectEndpoints(loggers, j.connecter)
|
|
||||||
defer j.mode.DisconnectEndpoints()
|
defer j.mode.DisconnectEndpoints()
|
||||||
|
|
||||||
// allow cancellation of an invocation (this function)
|
// allow cancellation of an invocation (this function)
|
||||||
@@ -376,20 +477,26 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "replication")
|
||||||
ctx, repCancel := context.WithCancel(ctx)
|
ctx, repCancel := context.WithCancel(ctx)
|
||||||
var repWait driver.WaitFunc
|
var repWait driver.WaitFunc
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
// reset it
|
// reset it
|
||||||
*tasks = activeSideTasks{}
|
*tasks = activeSideTasks{}
|
||||||
tasks.replicationCancel = repCancel
|
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
||||||
tasks.replicationReport, repWait = replication.Do(
|
tasks.replicationReport, repWait = replication.Do(
|
||||||
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver),
|
ctx, j.replicationDriverConfig, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||||
)
|
)
|
||||||
tasks.state = ActiveSideReplicating
|
tasks.state = ActiveSideReplicating
|
||||||
})
|
})
|
||||||
log.Info("start replication")
|
GetLogger(ctx).Info("start replication")
|
||||||
repWait(true) // wait blocking
|
repWait(true) // wait blocking
|
||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
|
|
||||||
|
replicationReport := j.tasks.replicationReport()
|
||||||
|
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
||||||
|
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -398,16 +505,18 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "prune_sender")
|
||||||
ctx, senderCancel := context.WithCancel(ctx)
|
ctx, senderCancel := context.WithCancel(ctx)
|
||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||||
tasks.prunerSenderCancel = senderCancel
|
tasks.prunerSenderCancel = func() { senderCancel(); endSpan() }
|
||||||
tasks.state = ActiveSidePruneSender
|
tasks.state = ActiveSidePruneSender
|
||||||
})
|
})
|
||||||
log.Info("start pruning sender")
|
GetLogger(ctx).Info("start pruning sender")
|
||||||
tasks.prunerSender.Prune()
|
tasks.prunerSender.Prune()
|
||||||
log.Info("finished pruning sender")
|
GetLogger(ctx).Info("finished pruning sender")
|
||||||
senderCancel()
|
senderCancel()
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
select {
|
select {
|
||||||
@@ -415,16 +524,18 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "prune_recever")
|
||||||
ctx, receiverCancel := context.WithCancel(ctx)
|
ctx, receiverCancel := context.WithCancel(ctx)
|
||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
||||||
tasks.prunerReceiverCancel = receiverCancel
|
tasks.prunerReceiverCancel = func() { receiverCancel(); endSpan() }
|
||||||
tasks.state = ActiveSidePruneReceiver
|
tasks.state = ActiveSidePruneReceiver
|
||||||
})
|
})
|
||||||
log.Info("start pruning receiver")
|
GetLogger(ctx).Info("start pruning receiver")
|
||||||
tasks.prunerReceiver.Prune()
|
tasks.prunerReceiver.Prune()
|
||||||
log.Info("finished pruning receiver")
|
GetLogger(ctx).Info("finished pruning receiver")
|
||||||
receiverCancel()
|
receiverCancel()
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFakeActiveSideDirectMethodInvocationClientIdentityDoesNotPassValidityTest(t *testing.T) {
|
||||||
|
jobid, err := endpoint.MakeJobID("validjobname")
|
||||||
|
require.NoError(t, err)
|
||||||
|
clientIdentity := FakeActiveSideDirectMethodInvocationClientIdentity(jobid)
|
||||||
|
t.Logf("%v", clientIdentity)
|
||||||
|
err = transport.ValidateClientIdentity(clientIdentity)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
+20
-20
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
||||||
@@ -48,20 +49,12 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
|||||||
// FIXME prettify this
|
// FIXME prettify this
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SinkJob:
|
case *config.SinkJob:
|
||||||
m, err := modeSinkFromConfig(c, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.SourceJob:
|
case *config.SourceJob:
|
||||||
m, err := modeSourceFromConfig(c, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -71,20 +64,12 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
|||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
m, err := modePushFromConfig(c, v)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = activeSide(c, &v.ActiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PullJob:
|
case *config.PullJob:
|
||||||
m, err := modePullFromConfig(c, v)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = activeSide(c, &v.ActiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -104,6 +89,11 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
sort.Slice(rfss, func(i, j int) bool {
|
sort.Slice(rfss, func(i, j int) bool {
|
||||||
return strings.Compare(rfss[i], rfss[j]) == -1
|
return strings.Compare(rfss[i], rfss[j]) == -1
|
||||||
})
|
})
|
||||||
|
// add tailing slash because of hierarchy-simulation
|
||||||
|
// rootfs/ is not root of rootfs2/
|
||||||
|
for i := 0; i < len(rfss); i++ {
|
||||||
|
rfss[i] += "/"
|
||||||
|
}
|
||||||
// idea:
|
// idea:
|
||||||
// no path in rfss must be prefix of another
|
// no path in rfss must be prefix of another
|
||||||
//
|
//
|
||||||
@@ -118,3 +108,13 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildBandwidthLimitConfig(in *config.BandwidthLimit) (c bandwidthlimit.Config, _ error) {
|
||||||
|
if in.Max.ToBytes() > 0 && int64(in.Max.ToBytes()) == 0 {
|
||||||
|
return c, fmt.Errorf("bandwidth limit `max` is too small, must at least specify one byte")
|
||||||
|
}
|
||||||
|
return bandwidthlimit.Config{
|
||||||
|
Max: int64(in.Max.ToBytes()),
|
||||||
|
BucketCapacity: int64(in.BucketCapacity.ToBytes()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SendingJobConfig interface {
|
||||||
|
GetFilesystems() config.FilesystemsFilter
|
||||||
|
GetSendOptions() *config.SendOptions // must not be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.SenderConfig, error) {
|
||||||
|
|
||||||
|
fsf, err := filters.DatasetMapFilterFromConfig(in.GetFilesystems())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
|
}
|
||||||
|
sendOpts := in.GetSendOptions()
|
||||||
|
bwlim, err := buildBandwidthLimitConfig(sendOpts.BandwidthLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build bandwith limit config")
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
JobID: jobID,
|
||||||
|
|
||||||
|
Encrypt: &nodefault.Bool{B: sendOpts.Encrypted},
|
||||||
|
SendRaw: sendOpts.Raw,
|
||||||
|
SendProperties: sendOpts.SendProperties,
|
||||||
|
SendBackupProperties: sendOpts.BackupProperties,
|
||||||
|
SendLargeBlocks: sendOpts.LargeBlocks,
|
||||||
|
SendCompressed: sendOpts.Compressed,
|
||||||
|
SendEmbeddedData: sendOpts.EmbeddedData,
|
||||||
|
SendSaved: sendOpts.Saved,
|
||||||
|
|
||||||
|
BandwidthLimit: bwlim,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build sender config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceivingJobConfig interface {
|
||||||
|
GetRootFS() string
|
||||||
|
GetAppendClientIdentity() bool
|
||||||
|
GetRecvOptions() *config.RecvOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoint.ReceiverConfig, err error) {
|
||||||
|
rootFs, err := zfs.NewDatasetPath(in.GetRootFS())
|
||||||
|
if err != nil {
|
||||||
|
return rc, errors.New("root_fs is not a valid zfs filesystem path")
|
||||||
|
}
|
||||||
|
if rootFs.Length() <= 0 {
|
||||||
|
return rc, errors.New("root_fs must not be empty") // duplicates error check of receiver
|
||||||
|
}
|
||||||
|
|
||||||
|
recvOpts := in.GetRecvOptions()
|
||||||
|
|
||||||
|
bwlim, err := buildBandwidthLimitConfig(recvOpts.BandwidthLimit)
|
||||||
|
if err != nil {
|
||||||
|
return rc, errors.Wrap(err, "cannot build bandwith limit config")
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: rootFs,
|
||||||
|
AppendClientIdentity: in.GetAppendClientIdentity(),
|
||||||
|
|
||||||
|
InheritProperties: recvOpts.Properties.Inherit,
|
||||||
|
OverrideProperties: recvOpts.Properties.Override,
|
||||||
|
|
||||||
|
BandwidthLimit: bwlim,
|
||||||
|
}
|
||||||
|
if err := rc.Validate(); err != nil {
|
||||||
|
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/transport/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||||
@@ -18,6 +26,7 @@ func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
|||||||
{false, []string{"a"}},
|
{false, []string{"a"}},
|
||||||
{false, []string{"some/path"}},
|
{false, []string{"some/path"}},
|
||||||
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
|
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
|
||||||
|
{false, []string{"zroot/foo", "zroot/foobar"}},
|
||||||
{true, []string{"zroot/b", "zroot/b"}},
|
{true, []string{"zroot/b", "zroot/b"}},
|
||||||
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
|
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
|
||||||
{false, []string{"a/x", "b/x"}},
|
{false, []string{"a/x", "b/x"}},
|
||||||
@@ -40,3 +49,272 @@ func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJobIDErrorHandling(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: %s
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: foo
|
||||||
|
client_identity: bar
|
||||||
|
filesystems: {"<": true}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
type Case struct {
|
||||||
|
jobName string
|
||||||
|
valid bool
|
||||||
|
}
|
||||||
|
cases := []Case{
|
||||||
|
{"validjobname", true},
|
||||||
|
{"valid with spaces", true},
|
||||||
|
{"invalid\twith\ttabs", false},
|
||||||
|
{"invalid#withdelimiter", false},
|
||||||
|
{"invalid@withdelimiter", false},
|
||||||
|
{"withnewline\\nmiddle", false},
|
||||||
|
{"withnewline\\n", false},
|
||||||
|
{"withslash/", false},
|
||||||
|
{"withslash/inthemiddle", false},
|
||||||
|
{"/", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
t.Run(cases[i].jobName, func(t *testing.T) {
|
||||||
|
c := cases[i]
|
||||||
|
|
||||||
|
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||||
|
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||||
|
require.NotNil(t, conf)
|
||||||
|
jobs, err := JobsFromConfig(conf)
|
||||||
|
|
||||||
|
if c.valid {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Len(t, jobs, 1)
|
||||||
|
assert.Equal(t, c.jobName, jobs[0].Name())
|
||||||
|
} else {
|
||||||
|
t.Logf("error: %s", err)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, jobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
||||||
|
paths, err := filepath.Glob("../../config/samples/*")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("glob failed: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type additionalCheck struct {
|
||||||
|
state int
|
||||||
|
test func(t *testing.T, jobs []Job)
|
||||||
|
}
|
||||||
|
additionalChecks := map[string]*additionalCheck{
|
||||||
|
"bandwidth_limit.yml": {test: testSampleConfig_BandwidthLimit},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range paths {
|
||||||
|
|
||||||
|
if path.Ext(p) != ".yml" {
|
||||||
|
t.Logf("skipping file %s", p)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := path.Base(p)
|
||||||
|
t.Logf("checking for presence additonal checks for file %q", filename)
|
||||||
|
additionalCheck := additionalChecks[filename]
|
||||||
|
if additionalCheck == nil {
|
||||||
|
t.Logf("no additional checks")
|
||||||
|
} else {
|
||||||
|
t.Logf("additional check present")
|
||||||
|
additionalCheck.state = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(p, func(t *testing.T) {
|
||||||
|
c, err := config.ParseConfig(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error parsing %s:\n%+v", p, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("file: %s", p)
|
||||||
|
t.Log(pretty.Sprint(c))
|
||||||
|
|
||||||
|
tls.FakeCertificateLoading(t)
|
||||||
|
jobs, err := JobsFromConfig(c)
|
||||||
|
t.Logf("jobs: %#v", jobs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if additionalCheck != nil {
|
||||||
|
additionalCheck.test(t, jobs)
|
||||||
|
additionalCheck.state = 2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for basename, c := range additionalChecks {
|
||||||
|
if c.state == 0 {
|
||||||
|
panic("univisited additional check " + basename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSampleConfig_BandwidthLimit(t *testing.T, jobs []Job) {
|
||||||
|
require.Len(t, jobs, 3)
|
||||||
|
|
||||||
|
{
|
||||||
|
limitedSink, ok := jobs[0].(*PassiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[0])
|
||||||
|
limitedSinkMode, ok := limitedSink.mode.(*modeSink)
|
||||||
|
require.True(t, ok, "%T", limitedSink)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(12345), limitedSinkMode.receiverConfig.BandwidthLimit.Max)
|
||||||
|
assert.Equal(t, int64(1<<17), limitedSinkMode.receiverConfig.BandwidthLimit.BucketCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
limitedPush, ok := jobs[1].(*ActiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[1])
|
||||||
|
limitedPushMode, ok := limitedPush.mode.(*modePush)
|
||||||
|
require.True(t, ok, "%T", limitedPush)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(54321), limitedPushMode.senderConfig.BandwidthLimit.Max)
|
||||||
|
assert.Equal(t, int64(1024), limitedPushMode.senderConfig.BandwidthLimit.BucketCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
unlimitedSink, ok := jobs[2].(*PassiveSide)
|
||||||
|
require.True(t, ok, "%T", jobs[2])
|
||||||
|
unlimitedSinkMode, ok := unlimitedSink.mode.(*modeSink)
|
||||||
|
require.True(t, ok, "%T", unlimitedSink)
|
||||||
|
|
||||||
|
max := unlimitedSinkMode.receiverConfig.BandwidthLimit.Max
|
||||||
|
assert.Less(t, max, int64(0), max, "unlimited mode <=> negative value for .Max, see bandwidthlimit.Config")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplicationOptions(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: foo
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: foo
|
||||||
|
client_identity: bar
|
||||||
|
filesystems: {"<": true}
|
||||||
|
%s
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectOk func(t *testing.T, a *ActiveSide, m *modePush)
|
||||||
|
expectError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []Test{
|
||||||
|
{
|
||||||
|
name: "defaults",
|
||||||
|
input: `
|
||||||
|
replication: {}
|
||||||
|
`,
|
||||||
|
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "steps_zero",
|
||||||
|
input: `
|
||||||
|
replication:
|
||||||
|
concurrency:
|
||||||
|
steps: 0
|
||||||
|
`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size_estimates_zero",
|
||||||
|
input: `
|
||||||
|
replication:
|
||||||
|
concurrency:
|
||||||
|
size_estimates: 0
|
||||||
|
`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom_values",
|
||||||
|
input: `
|
||||||
|
replication:
|
||||||
|
concurrency:
|
||||||
|
steps: 23
|
||||||
|
size_estimates: 42
|
||||||
|
`,
|
||||||
|
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {
|
||||||
|
assert.Equal(t, 23, a.replicationDriverConfig.StepQueueConcurrency)
|
||||||
|
assert.Equal(t, 42, m.plannerPolicy.SizeEstimationConcurrency)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative_values_forbidden",
|
||||||
|
input: `
|
||||||
|
replication:
|
||||||
|
concurrency:
|
||||||
|
steps: -23
|
||||||
|
size_estimates: -42
|
||||||
|
`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
for _, ts := range tests {
|
||||||
|
t.Run(ts.name, func(t *testing.T) {
|
||||||
|
assert.True(t, (ts.expectError) != (ts.expectOk != nil))
|
||||||
|
|
||||||
|
cstr := fill(ts.input)
|
||||||
|
t.Logf("testing config:\n%s", cstr)
|
||||||
|
c, err := config.ParseConfigBytes([]byte(cstr))
|
||||||
|
require.NoError(t, err)
|
||||||
|
jobs, err := JobsFromConfig(c)
|
||||||
|
if ts.expectOk != nil {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, jobs, 1)
|
||||||
|
a := jobs[0].(*ActiveSide)
|
||||||
|
m := a.mode.(*modePush)
|
||||||
|
ts.expectOk(t, a, m)
|
||||||
|
} else if ts.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("test must define expectOk or expectError")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
+4
-14
@@ -7,27 +7,16 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Logger = logger.Logger
|
type Logger = logger.Logger
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyLog contextKey = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetLogger(ctx context.Context) Logger {
|
func GetLogger(ctx context.Context) Logger {
|
||||||
if l, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
return logging.GetLogger(ctx, logging.SubsysJob)
|
||||||
return l
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithLogger(ctx context.Context, l Logger) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyLog, l)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Job interface {
|
type Job interface {
|
||||||
@@ -38,6 +27,7 @@ type Job interface {
|
|||||||
// Jobs that return a subtree of the dataset hierarchy
|
// Jobs that return a subtree of the dataset hierarchy
|
||||||
// must return the root of that subtree as rfs and ok = true
|
// must return the root of that subtree as rfs and ok = true
|
||||||
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
||||||
|
SenderConfig() *endpoint.SenderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type Type string
|
type Type string
|
||||||
|
|||||||
+62
-29
@@ -7,8 +7,9 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -20,7 +21,7 @@ import (
|
|||||||
|
|
||||||
type PassiveSide struct {
|
type PassiveSide struct {
|
||||||
mode passiveMode
|
mode passiveMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
listen transport.AuthenticatedListenerFactory
|
listen transport.AuthenticatedListenerFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,45 +33,44 @@ type passiveMode interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modeSink struct {
|
type modeSink struct {
|
||||||
rootDataset *zfs.DatasetPath
|
receiverConfig endpoint.ReceiverConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSink) Type() Type { return TypeSink }
|
func (m *modeSink) Type() Type { return TypeSink }
|
||||||
|
|
||||||
func (m *modeSink) Handler() rpc.Handler {
|
func (m *modeSink) Handler() rpc.Handler {
|
||||||
return endpoint.NewReceiver(m.rootDataset, true)
|
return endpoint.NewReceiver(m.receiverConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
||||||
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||||
|
|
||||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob) (m *modeSink, err error) {
|
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||||
m = &modeSink{}
|
m = &modeSink{}
|
||||||
m.rootDataset, err = zfs.NewDatasetPath(in.RootFS)
|
|
||||||
|
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
return nil, err
|
||||||
}
|
|
||||||
if m.rootDataset.Length() <= 0 {
|
|
||||||
return nil, errors.New("root dataset must not be empty") // duplicates error check of receiver
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type modeSource struct {
|
type modeSource struct {
|
||||||
fsfilter zfs.DatasetFilter
|
senderConfig *endpoint.SenderConfig
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource, err error) {
|
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||||
// FIXME exact dedup of modePush
|
// FIXME exact dedup of modePush
|
||||||
m = &modeSource{}
|
m = &modeSource{}
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
|
||||||
}
|
|
||||||
m.fsfilter = fsf
|
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "send options")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource
|
|||||||
func (m *modeSource) Type() Type { return TypeSource }
|
func (m *modeSource) Type() Type { return TypeSource }
|
||||||
|
|
||||||
func (m *modeSource) Handler() rpc.Handler {
|
func (m *modeSource) Handler() rpc.Handler {
|
||||||
return endpoint.NewSender(m.fsfilter)
|
return endpoint.NewSender(*m.senderConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
||||||
@@ -91,9 +91,25 @@ func (m *modeSource) SnapperReport() *snapper.Report {
|
|||||||
return m.snapper.Report()
|
return m.snapper.Report()
|
||||||
}
|
}
|
||||||
|
|
||||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}) (s *PassiveSide, err error) {
|
||||||
|
|
||||||
|
s = &PassiveSide{}
|
||||||
|
|
||||||
|
s.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := configJob.(type) {
|
||||||
|
case *config.SinkJob:
|
||||||
|
s.mode, err = modeSinkFromConfig(g, v, s.name) // shadow
|
||||||
|
case *config.SourceJob:
|
||||||
|
s.mode, err = modeSourceFromConfig(g, v, s.name) // shadow
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // no wrapping necessary
|
||||||
|
}
|
||||||
|
|
||||||
s = &PassiveSide{mode: mode, name: in.Name}
|
|
||||||
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build listener factory")
|
return nil, errors.Wrap(err, "cannot build listener factory")
|
||||||
}
|
}
|
||||||
@@ -101,7 +117,7 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passive
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PassiveSide) Name() string { return j.name }
|
func (j *PassiveSide) Name() string { return j.name.String() }
|
||||||
|
|
||||||
type PassiveStatus struct {
|
type PassiveStatus struct {
|
||||||
Snapper *snapper.Report
|
Snapper *snapper.Report
|
||||||
@@ -120,18 +136,29 @@ func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
|||||||
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
|
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return sink.rootDataset.Copy(), true
|
return sink.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
|
source, ok := j.mode.(*modeSource)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modeSink) // make sure we didn't introduce a new job type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return source.senderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||||
|
|
||||||
func (j *PassiveSide) Run(ctx context.Context) {
|
func (j *PassiveSide) Run(ctx context.Context) {
|
||||||
|
ctx, endTask := trace.WithTaskAndSpan(ctx, "passive-side-job", j.Name())
|
||||||
|
defer endTask()
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
|
||||||
{
|
{
|
||||||
ctx, cancel := context.WithCancel(ctx) // shadowing
|
ctx, endTask := trace.WithTask(ctx, "periodic") // shadowing
|
||||||
|
defer endTask()
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go j.mode.RunPeriodic(ctx)
|
go j.mode.RunPeriodic(ctx)
|
||||||
}
|
}
|
||||||
@@ -141,8 +168,14 @@ func (j *PassiveSide) Run(ctx context.Context) {
|
|||||||
panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
|
panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxInterceptor := func(handlerCtx context.Context) context.Context {
|
ctxInterceptor := func(handlerCtx context.Context, info rpc.HandlerContextInterceptorData, handler func(ctx context.Context)) {
|
||||||
return logging.WithSubsystemLoggers(handlerCtx, log)
|
// the handlerCtx is clean => need to inherit logging and tracing config from job context
|
||||||
|
handlerCtx = logging.WithInherit(handlerCtx, ctx)
|
||||||
|
handlerCtx = trace.WithInherit(handlerCtx, ctx)
|
||||||
|
|
||||||
|
handlerCtx, endTask := trace.WithTaskAndSpan(handlerCtx, "handler", fmt.Sprintf("job=%q client=%q method=%q", j.Name(), info.ClientIdentity(), info.FullMethod()))
|
||||||
|
defer endTask()
|
||||||
|
handler(handlerCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
|
rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
|
||||||
|
|||||||
+42
-17
@@ -4,14 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
||||||
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -20,7 +24,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SnapJob struct {
|
type SnapJob struct {
|
||||||
name string
|
name endpoint.JobID
|
||||||
fsfilter zfs.DatasetFilter
|
fsfilter zfs.DatasetFilter
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper *snapper.PeriodicOrManual
|
||||||
|
|
||||||
@@ -28,10 +32,11 @@ type SnapJob struct {
|
|||||||
|
|
||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
|
|
||||||
pruner *pruner.Pruner
|
prunerMtx sync.Mutex
|
||||||
|
pruner *pruner.Pruner
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SnapJob) Name() string { return j.name }
|
func (j *SnapJob) Name() string { return j.name.String() }
|
||||||
|
|
||||||
func (j *SnapJob) Type() Type { return TypeSnap }
|
func (j *SnapJob) Type() Type { return TypeSnap }
|
||||||
|
|
||||||
@@ -39,20 +44,23 @@ func snapJobFromConfig(g *config.Global, in *config.SnapJob) (j *SnapJob, err er
|
|||||||
j = &SnapJob{}
|
j = &SnapJob{}
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
}
|
}
|
||||||
j.fsfilter = fsf
|
j.fsfilter = fsf
|
||||||
|
|
||||||
if j.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
if j.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
}
|
}
|
||||||
j.name = in.Name
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "pruning",
|
Subsystem: "pruning",
|
||||||
Name: "time",
|
Name: "time",
|
||||||
Help: "seconds spent in pruner",
|
Help: "seconds spent in pruner",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"prune_side"})
|
}, []string{"prune_side"})
|
||||||
j.prunerFactory, err = pruner.NewLocalPrunerFactory(in.Pruning, j.promPruneSecs)
|
j.prunerFactory, err = pruner.NewLocalPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,9 +81,11 @@ type SnapJobStatus struct {
|
|||||||
func (j *SnapJob) Status() *Status {
|
func (j *SnapJob) Status() *Status {
|
||||||
s := &SnapJobStatus{}
|
s := &SnapJobStatus{}
|
||||||
t := j.Type()
|
t := j.Type()
|
||||||
|
j.prunerMtx.Lock()
|
||||||
if j.pruner != nil {
|
if j.pruner != nil {
|
||||||
s.Pruning = j.pruner.Report()
|
s.Pruning = j.pruner.Report()
|
||||||
}
|
}
|
||||||
|
j.prunerMtx.Unlock()
|
||||||
s.Snapshotting = j.snapper.Report()
|
s.Snapshotting = j.snapper.Report()
|
||||||
return &Status{Type: t, JobSpecific: s}
|
return &Status{Type: t, JobSpecific: s}
|
||||||
}
|
}
|
||||||
@@ -84,16 +94,21 @@ func (j *SnapJob) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *SnapJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
func (j *SnapJob) Run(ctx context.Context) {
|
func (j *SnapJob) Run(ctx context.Context) {
|
||||||
|
ctx, endTask := trace.WithTaskAndSpan(ctx, "snap-job", j.Name())
|
||||||
|
defer endTask()
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
|
||||||
|
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
|
||||||
periodicDone := make(chan struct{})
|
periodicDone := make(chan struct{})
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go j.snapper.Run(ctx, periodicDone)
|
periodicCtx, endTask := trace.WithTask(ctx, "snapshotting")
|
||||||
|
defer endTask()
|
||||||
|
go j.snapper.Run(periodicCtx, periodicDone)
|
||||||
|
|
||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
@@ -108,8 +123,10 @@ outer:
|
|||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
invLog := log.WithField("invocation", invocationCount)
|
|
||||||
j.doPrune(WithLogger(ctx, invLog))
|
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
||||||
|
j.doPrune(invocationCtx)
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +147,9 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
|||||||
target pruner.Target
|
target pruner.Target
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
var _ pruner.Sender = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
||||||
|
|
||||||
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
if req.GetGet() == nil {
|
|
||||||
return nil, fmt.Errorf("unsupported ReplicationCursor request: SnapJob only supports GETting a (faked) cursor")
|
|
||||||
}
|
|
||||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||||
Filesystem: req.GetFilesystem(),
|
Filesystem: req.GetFilesystem(),
|
||||||
}
|
}
|
||||||
@@ -160,10 +174,21 @@ func (h alwaysUpToDateReplicationCursorHistory) ListFilesystems(ctx context.Cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (j *SnapJob) doPrune(ctx context.Context) {
|
func (j *SnapJob) doPrune(ctx context.Context) {
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "snap-job-do-prune")
|
||||||
|
defer endSpan()
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||||
sender := endpoint.NewSender(j.fsfilter)
|
JobID: j.name,
|
||||||
|
FSF: j.fsfilter,
|
||||||
|
// FIXME the following config fields are irrelevant for SnapJob
|
||||||
|
// because the endpoint is only used as pruner.Target.
|
||||||
|
// However, the implementation requires them to be set.
|
||||||
|
Encrypt: &nodefault.Bool{B: true},
|
||||||
|
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
||||||
|
})
|
||||||
|
j.prunerMtx.Lock()
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
|
j.prunerMtx.Unlock()
|
||||||
log.Info("start pruning")
|
log.Info("start pruning")
|
||||||
j.pruner.Prune()
|
j.pruner.Prune()
|
||||||
log.Info("finished pruning")
|
log.Info("finished pruning")
|
||||||
|
|||||||
@@ -11,17 +11,9 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/replication/driver"
|
|
||||||
"github.com/zrepl/zrepl/replication/logic"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/rpc/transportmux"
|
|
||||||
"github.com/zrepl/zrepl/tlsconf"
|
"github.com/zrepl/zrepl/tlsconf"
|
||||||
"github.com/zrepl/zrepl/transport"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
||||||
@@ -69,8 +61,10 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
|||||||
type Subsystem string
|
type Subsystem string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
SubsysMeta Subsystem = "meta"
|
||||||
|
SubsysJob Subsystem = "job"
|
||||||
SubsysReplication Subsystem = "repl"
|
SubsysReplication Subsystem = "repl"
|
||||||
SubsyEndpoint Subsystem = "endpoint"
|
SubsysEndpoint Subsystem = "endpoint"
|
||||||
SubsysPruning Subsystem = "pruning"
|
SubsysPruning Subsystem = "pruning"
|
||||||
SubsysSnapshot Subsystem = "snapshot"
|
SubsysSnapshot Subsystem = "snapshot"
|
||||||
SubsysHooks Subsystem = "hook"
|
SubsysHooks Subsystem = "hook"
|
||||||
@@ -79,29 +73,104 @@ const (
|
|||||||
SubsysRPC Subsystem = "rpc"
|
SubsysRPC Subsystem = "rpc"
|
||||||
SubsysRPCControl Subsystem = "rpc.ctrl"
|
SubsysRPCControl Subsystem = "rpc.ctrl"
|
||||||
SubsysRPCData Subsystem = "rpc.data"
|
SubsysRPCData Subsystem = "rpc.data"
|
||||||
|
SubsysZFSCmd Subsystem = "zfs.cmd"
|
||||||
|
SubsysTraceData Subsystem = "trace.data"
|
||||||
|
SubsysPlatformtest Subsystem = "platformtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
var AllSubsystems = []Subsystem{
|
||||||
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
SubsysMeta,
|
||||||
ctx = driver.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
SubsysJob,
|
||||||
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
|
SubsysReplication,
|
||||||
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
SubsysEndpoint,
|
||||||
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
SubsysPruning,
|
||||||
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
SubsysSnapshot,
|
||||||
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
SubsysHooks,
|
||||||
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
SubsysTransport,
|
||||||
ctx = rpc.WithLoggers(ctx,
|
SubsysTransportMux,
|
||||||
rpc.Loggers{
|
SubsysRPC,
|
||||||
General: log.WithField(SubsysField, SubsysRPC),
|
SubsysRPCControl,
|
||||||
Control: log.WithField(SubsysField, SubsysRPCControl),
|
SubsysRPCData,
|
||||||
Data: log.WithField(SubsysField, SubsysRPCData),
|
SubsysZFSCmd,
|
||||||
},
|
SubsysTraceData,
|
||||||
)
|
SubsysPlatformtest,
|
||||||
return ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogSubsystem(log logger.Logger, subsys Subsystem) logger.Logger {
|
type injectedField struct {
|
||||||
return log.ReplaceField(SubsysField, subsys)
|
field string
|
||||||
|
value interface{}
|
||||||
|
parent *injectedField
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithInjectedField(ctx context.Context, field string, value interface{}) context.Context {
|
||||||
|
var parent *injectedField
|
||||||
|
parentI := ctx.Value(contextKeyInjectedField)
|
||||||
|
if parentI != nil {
|
||||||
|
parent = parentI.(*injectedField)
|
||||||
|
}
|
||||||
|
// TODO sanity-check `field` now
|
||||||
|
this := &injectedField{field, value, parent}
|
||||||
|
return context.WithValue(ctx, contextKeyInjectedField, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
func iterInjectedFields(ctx context.Context, cb func(field string, value interface{})) {
|
||||||
|
injI := ctx.Value(contextKeyInjectedField)
|
||||||
|
if injI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inj := injI.(*injectedField)
|
||||||
|
for ; inj != nil; inj = inj.parent {
|
||||||
|
cb(inj.field, inj.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubsystemLoggers map[Subsystem]logger.Logger
|
||||||
|
|
||||||
|
func SubsystemLoggersWithUniversalLogger(l logger.Logger) SubsystemLoggers {
|
||||||
|
loggers := make(SubsystemLoggers)
|
||||||
|
for _, s := range AllSubsystems {
|
||||||
|
loggers[s] = l
|
||||||
|
}
|
||||||
|
return loggers
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLoggers(ctx context.Context, loggers SubsystemLoggers) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyLoggers, loggers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLoggers(ctx context.Context) SubsystemLoggers {
|
||||||
|
loggers, ok := ctx.Value(contextKeyLoggers).(SubsystemLoggers)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return loggers
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogger(ctx context.Context, subsys Subsystem) logger.Logger {
|
||||||
|
return getLoggerImpl(ctx, subsys, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLoggerImpl(ctx context.Context, subsys Subsystem, panicIfEnded bool) logger.Logger {
|
||||||
|
loggers, ok := ctx.Value(contextKeyLoggers).(SubsystemLoggers)
|
||||||
|
if !ok || loggers == nil {
|
||||||
|
return logger.NewNullLogger()
|
||||||
|
}
|
||||||
|
l, ok := loggers[subsys]
|
||||||
|
if !ok {
|
||||||
|
return logger.NewNullLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.WithField(SubsysField, subsys)
|
||||||
|
|
||||||
|
l = l.WithField(SpanField, trace.GetSpanStackOrDefault(ctx, *trace.StackKindId, "NOSPAN"))
|
||||||
|
|
||||||
|
fields := make(logger.Fields)
|
||||||
|
iterInjectedFields(ctx, func(field string, value interface{}) {
|
||||||
|
fields[field] = value
|
||||||
|
})
|
||||||
|
l = l.WithFields(fields)
|
||||||
|
|
||||||
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeyLoggers contextKey = 1 + iota
|
||||||
|
contextKeyInjectedField
|
||||||
|
)
|
||||||
|
|
||||||
|
var contextKeys = []contextKey{
|
||||||
|
contextKeyLoggers,
|
||||||
|
contextKeyInjectedField,
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithInherit(ctx, inheritFrom context.Context) context.Context {
|
||||||
|
for _, k := range contextKeys {
|
||||||
|
if v := inheritFrom.Value(k); v != nil {
|
||||||
|
ctx = context.WithValue(ctx, k, v) // no shadow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
JobField string = "job"
|
JobField string = "job"
|
||||||
SubsysField string = "subsystem"
|
SubsysField string = "subsystem"
|
||||||
|
SpanField string = "span"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetadataFlags int64
|
type MetadataFlags int64
|
||||||
@@ -85,7 +86,7 @@ func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
|
|||||||
fmt.Fprintf(&line, "[%s]", col.Sprint(e.Level.Short()))
|
fmt.Fprintf(&line, "[%s]", col.Sprint(e.Level.Short()))
|
||||||
}
|
}
|
||||||
|
|
||||||
prefixFields := []string{JobField, SubsysField}
|
prefixFields := []string{JobField, SubsysField, SpanField}
|
||||||
prefixed := make(map[string]bool, len(prefixFields)+2)
|
prefixed := make(map[string]bool, len(prefixFields)+2)
|
||||||
for _, field := range prefixFields {
|
for _, field := range prefixFields {
|
||||||
val, ok := e.Fields[field]
|
val, ok := e.Fields[field]
|
||||||
@@ -174,8 +175,8 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// at least try and put job and task in front
|
// at least try and put job and task in front
|
||||||
prefixed := make(map[string]bool, 2)
|
prefixed := make(map[string]bool, 3)
|
||||||
prefix := []string{JobField, SubsysField}
|
prefix := []string{JobField, SubsysField, SpanField}
|
||||||
for _, pf := range prefix {
|
for _, pf := range prefix {
|
||||||
v, ok := e.Fields[pf]
|
v, ok := e.Fields[pf]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -211,7 +212,7 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
|
|||||||
case logfmt.ErrUnsupportedValueType:
|
case logfmt.ErrUnsupportedValueType:
|
||||||
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot encode unsuuported value type Go type")
|
return errors.Wrap(err, "cannot encode unsupported value type Go type")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func NewTCPOutlet(formatter EntryFormatter, network, address string, tlsConfig *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previos is in io.Copy()
|
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previous is in io.Copy()
|
||||||
|
|
||||||
o := &TCPOutlet{
|
o := &TCPOutlet{
|
||||||
formatter: formatter,
|
formatter: formatter,
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
// package trace provides activity tracing via ctx through Tasks and Spans
|
||||||
|
//
|
||||||
|
// Basic Concepts
|
||||||
|
//
|
||||||
|
// Tracing can be used to identify where a piece of code spends its time.
|
||||||
|
//
|
||||||
|
// The Go standard library provides package runtime/trace which is useful to identify CPU bottlenecks or
|
||||||
|
// to understand what happens inside the Go runtime.
|
||||||
|
// However, it is not ideal for application level tracing, in particular if those traces should be understandable
|
||||||
|
// to tech-savvy users (albeit not developers).
|
||||||
|
//
|
||||||
|
// This package provides the concept of Tasks and Spans to express what activity is happening within an application:
|
||||||
|
//
|
||||||
|
// - Neither task nor span is really tangible but instead contained within the context.Context tree
|
||||||
|
// - Tasks represent concurrent activity (i.e. goroutines).
|
||||||
|
// - Spans represent a semantic stack trace within a task.
|
||||||
|
//
|
||||||
|
// As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task:
|
||||||
|
//
|
||||||
|
// go func(ctx context.Context) {
|
||||||
|
// ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task")
|
||||||
|
// defer endTask()
|
||||||
|
// // ...
|
||||||
|
// }(ctx)
|
||||||
|
//
|
||||||
|
// Within the task, you can open up a hierarchy of spans.
|
||||||
|
// In contrast to tasks, which have can multiple concurrently running child tasks,
|
||||||
|
// spans must nest and not cross the goroutine boundary.
|
||||||
|
//
|
||||||
|
// ctx, endSpan = WithSpan(ctx, "copy-dir")
|
||||||
|
// defer endSpan()
|
||||||
|
// for _, f := range dir.Files() {
|
||||||
|
// func() {
|
||||||
|
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||||
|
// defer endspan()
|
||||||
|
// b, _ := ioutil.ReadFile(f)
|
||||||
|
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||||
|
// }()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// In combination:
|
||||||
|
// ctx, endTask = WithTask(ctx, "copy-dirs")
|
||||||
|
// defer endTask()
|
||||||
|
// for i := range dirs {
|
||||||
|
// go func(dir string) {
|
||||||
|
// ctx, endTask := WithTask(ctx, "copy-dir")
|
||||||
|
// defer endTask()
|
||||||
|
// for _, f := range filesIn(dir) {
|
||||||
|
// func() {
|
||||||
|
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||||
|
// defer endspan()
|
||||||
|
// b, _ := ioutil.ReadFile(f)
|
||||||
|
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||||
|
// }()
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note that a span ends at the time you call endSpan - not before and not after that.
|
||||||
|
// If you violate the stack-like nesting of spans by forgetting an endSpan() invocation,
|
||||||
|
// the out-of-order endSpan() will panic.
|
||||||
|
//
|
||||||
|
// A similar rule applies to the endTask closure returned by WithTask:
|
||||||
|
// If a task has live child tasks at the time you call endTask(), the call will panic.
|
||||||
|
//
|
||||||
|
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Best Practices For Naming Tasks And Spans
|
||||||
|
//
|
||||||
|
// Tasks should always have string constants as names, and must not contain the `#` character. WHy?
|
||||||
|
// First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace.
|
||||||
|
// Also, the package appends `#NUM` for each concurrently running instance of a task name.
|
||||||
|
// Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an
|
||||||
|
// infinite number of horizontal bars in the visualization.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Chrome-compatible Tracefile Support
|
||||||
|
//
|
||||||
|
// The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file
|
||||||
|
// that can be loaded into chrome://tracing .
|
||||||
|
// Apart from function GetSpanStackOrDefault, this is the main benefit of this package.
|
||||||
|
//
|
||||||
|
// First, there is a convenience environment variable 'ZREPL_ACTIVITY_TRACE' that can be set to an output path.
|
||||||
|
// From process start onward, a trace is written to that path.
|
||||||
|
//
|
||||||
|
// More consumers can attach to the activity trace through the ChrometraceClientWebsocketHandler websocket handler.
|
||||||
|
//
|
||||||
|
// If a write error is encountered with any consumer (including the env-var based one), the consumer is closed and
|
||||||
|
// will not receive further trace output.
|
||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
runtimedebug "runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var metrics struct {
|
||||||
|
activeTasks prometheus.Gauge
|
||||||
|
}
|
||||||
|
var taskNamer *uniqueConcurrentTaskNamer = newUniqueTaskNamer()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
metrics.activeTasks = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "zrepl",
|
||||||
|
Subsystem: "trace",
|
||||||
|
Name: "active_tasks",
|
||||||
|
Help: "number of active (tracing-level) tasks in the daemon",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterMetrics(r prometheus.Registerer) {
|
||||||
|
r.MustRegister(metrics.activeTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
type traceNode struct {
|
||||||
|
// NOTE: members with prefix debug are only valid if the package variable debugEnabled is true.
|
||||||
|
|
||||||
|
id string
|
||||||
|
annotation string
|
||||||
|
parentTask *traceNode
|
||||||
|
debugCreationStack string // debug: stack trace of when the traceNode was created
|
||||||
|
|
||||||
|
mtx chainlock.L
|
||||||
|
|
||||||
|
activeChildTasks int32 // only for task nodes, insignificant for span nodes
|
||||||
|
debugActiveChildTasks map[*traceNode]bool // debug: set of active child tasks
|
||||||
|
parentSpan *traceNode
|
||||||
|
activeChildSpan *traceNode // nil if task or span doesn't have an active child span
|
||||||
|
|
||||||
|
startedAt time.Time
|
||||||
|
endedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *traceNode) StartedAt() time.Time { return s.startedAt }
|
||||||
|
func (s *traceNode) EndedAt() time.Time { return s.endedAt }
|
||||||
|
|
||||||
|
// caller must hold mtx
|
||||||
|
func (s *traceNode) debugString() string {
|
||||||
|
if !debugEnabled {
|
||||||
|
return "<debugEnabled=false>"
|
||||||
|
}
|
||||||
|
return pretty.Sprint(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returned from WithTask or WithSpan.
|
||||||
|
// Must be called once the task or span ends.
|
||||||
|
// See package-level docs for nesting rules.
|
||||||
|
// Wrong call order / forgetting to call it will result in panics.
|
||||||
|
type DoneFunc func()
|
||||||
|
|
||||||
|
var ErrTaskStillHasActiveChildTasks = fmt.Errorf("end task: task still has active child tasks")
|
||||||
|
|
||||||
|
// Start a new root task or create a child task of an existing task.
|
||||||
|
//
|
||||||
|
// This is required when starting a new goroutine and
|
||||||
|
// passing an existing task context to it.
|
||||||
|
//
|
||||||
|
// taskName should be a constantand must not contain '#'
|
||||||
|
//
|
||||||
|
// The implementation ensures that,
|
||||||
|
// if multiple tasks with the same name exist simultaneously,
|
||||||
|
// a unique suffix is appended to uniquely identify the task opened with this function.
|
||||||
|
func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc) {
|
||||||
|
|
||||||
|
var parentTask *traceNode
|
||||||
|
nodeI := ctx.Value(contextKeyTraceNode)
|
||||||
|
if nodeI != nil {
|
||||||
|
node := nodeI.(*traceNode)
|
||||||
|
if node.parentSpan != nil {
|
||||||
|
parentTask = node.parentTask
|
||||||
|
} else {
|
||||||
|
parentTask = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// find the first ancestor that hasn't ended yet (nil if need be)
|
||||||
|
if parentTask != nil {
|
||||||
|
parentTask.mtx.Lock()
|
||||||
|
}
|
||||||
|
for parentTask != nil && !parentTask.endedAt.IsZero() {
|
||||||
|
thisParent := parentTask
|
||||||
|
parentTask = parentTask.parentTask
|
||||||
|
// lock parent first such that it isn't modified by other callers to thisParent
|
||||||
|
if parentTask != nil {
|
||||||
|
parentTask.mtx.Lock()
|
||||||
|
}
|
||||||
|
thisParent.mtx.Unlock()
|
||||||
|
}
|
||||||
|
// invariant: either parentTask != nil and we hold the lock on parentTask, or parentTask is nil
|
||||||
|
|
||||||
|
taskName, taskNameDone := taskNamer.UniqueConcurrentTaskName(taskName)
|
||||||
|
|
||||||
|
this := &traceNode{
|
||||||
|
id: genID(),
|
||||||
|
annotation: taskName,
|
||||||
|
parentTask: parentTask,
|
||||||
|
activeChildTasks: 0,
|
||||||
|
parentSpan: nil,
|
||||||
|
activeChildSpan: nil,
|
||||||
|
|
||||||
|
startedAt: time.Now(),
|
||||||
|
endedAt: time.Time{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugEnabled {
|
||||||
|
this.debugCreationStack = string(runtimedebug.Stack())
|
||||||
|
this.debugActiveChildTasks = map[*traceNode]bool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentTask != nil {
|
||||||
|
this.parentTask.activeChildTasks++
|
||||||
|
if debugEnabled {
|
||||||
|
this.parentTask.debugActiveChildTasks[this] = true
|
||||||
|
}
|
||||||
|
parentTask.mtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
|
||||||
|
|
||||||
|
chrometraceBeginTask(this)
|
||||||
|
|
||||||
|
metrics.activeTasks.Inc()
|
||||||
|
|
||||||
|
endTaskFunc := func() {
|
||||||
|
|
||||||
|
// only hold locks while manipulating the tree
|
||||||
|
// (trace writer might block too long and unlike spans, tasks are updated concurrently)
|
||||||
|
alreadyEnded := func() (alreadyEnded bool) {
|
||||||
|
if this.parentTask != nil {
|
||||||
|
defer this.parentTask.mtx.Lock().Unlock()
|
||||||
|
}
|
||||||
|
defer this.mtx.Lock().Unlock()
|
||||||
|
|
||||||
|
if this.activeChildTasks != 0 {
|
||||||
|
if debugEnabled {
|
||||||
|
// the debugString can be quite long and panic won't print it completely
|
||||||
|
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
||||||
|
}
|
||||||
|
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks\n", this.activeChildTasks))
|
||||||
|
}
|
||||||
|
|
||||||
|
// support idempotent task ends
|
||||||
|
if !this.endedAt.IsZero() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
this.endedAt = time.Now()
|
||||||
|
|
||||||
|
if this.parentTask != nil {
|
||||||
|
this.parentTask.activeChildTasks--
|
||||||
|
if debugEnabled {
|
||||||
|
delete(this.parentTask.debugActiveChildTasks, this)
|
||||||
|
}
|
||||||
|
if this.parentTask.activeChildTasks < 0 {
|
||||||
|
if debugEnabled {
|
||||||
|
// the debugString can be quite long and panic won't print it completely
|
||||||
|
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks < 0:\n%s\n", this.parentTask.debugString())
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("impl error: parent task with negative activeChildTasks count: %v", this.parentTask.activeChildTasks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()
|
||||||
|
if alreadyEnded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chrometraceEndTask(this)
|
||||||
|
|
||||||
|
metrics.activeTasks.Dec()
|
||||||
|
|
||||||
|
taskNameDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, endTaskFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrAlreadyActiveChildSpan = fmt.Errorf("create child span: span already has an active child span")
|
||||||
|
var ErrSpanStillHasActiveChildSpan = fmt.Errorf("end span: span still has active child spans")
|
||||||
|
|
||||||
|
// Start a new span.
|
||||||
|
// Important: ctx must have an active task (see WithTask)
|
||||||
|
func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc) {
|
||||||
|
var parentSpan, parentTask *traceNode
|
||||||
|
nodeI := ctx.Value(contextKeyTraceNode)
|
||||||
|
if nodeI != nil {
|
||||||
|
parentSpan = nodeI.(*traceNode)
|
||||||
|
if parentSpan.parentSpan == nil {
|
||||||
|
parentTask = parentSpan
|
||||||
|
} else {
|
||||||
|
parentTask = parentSpan.parentTask
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic("must be called from within a task")
|
||||||
|
}
|
||||||
|
|
||||||
|
this := &traceNode{
|
||||||
|
id: genID(),
|
||||||
|
annotation: annotation,
|
||||||
|
parentTask: parentTask,
|
||||||
|
parentSpan: parentSpan,
|
||||||
|
activeChildSpan: nil,
|
||||||
|
|
||||||
|
startedAt: time.Now(),
|
||||||
|
endedAt: time.Time{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugEnabled {
|
||||||
|
this.debugCreationStack = string(runtimedebug.Stack())
|
||||||
|
}
|
||||||
|
|
||||||
|
parentSpan.mtx.HoldWhile(func() {
|
||||||
|
if parentSpan.activeChildSpan != nil {
|
||||||
|
panic(ErrAlreadyActiveChildSpan)
|
||||||
|
}
|
||||||
|
parentSpan.activeChildSpan = this
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
|
||||||
|
chrometraceBeginSpan(this)
|
||||||
|
callbackEndSpan := callbackBeginSpan(ctx)
|
||||||
|
|
||||||
|
endTaskFunc := func() {
|
||||||
|
|
||||||
|
defer parentSpan.mtx.Lock().Unlock()
|
||||||
|
if parentSpan.activeChildSpan != this && this.endedAt.IsZero() {
|
||||||
|
panic("impl error: activeChildSpan should not change while != nil because there can only be one")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer this.mtx.Lock().Unlock()
|
||||||
|
if this.activeChildSpan != nil {
|
||||||
|
panic(ErrSpanStillHasActiveChildSpan)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !this.endedAt.IsZero() {
|
||||||
|
return // support idempotent span ends
|
||||||
|
}
|
||||||
|
|
||||||
|
parentSpan.activeChildSpan = nil
|
||||||
|
this.endedAt = time.Now()
|
||||||
|
|
||||||
|
chrometraceEndSpan(this)
|
||||||
|
callbackEndSpan(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, endTaskFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type StackKind struct {
|
||||||
|
symbolizeTask func(t *traceNode) string
|
||||||
|
symbolizeSpan func(s *traceNode) string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
StackKindId = &StackKind{
|
||||||
|
symbolizeTask: func(t *traceNode) string { return t.id },
|
||||||
|
symbolizeSpan: func(s *traceNode) string { return s.id },
|
||||||
|
}
|
||||||
|
SpanStackKindCombined = &StackKind{
|
||||||
|
symbolizeTask: func(t *traceNode) string { return fmt.Sprintf("(%s %q)", t.id, t.annotation) },
|
||||||
|
symbolizeSpan: func(s *traceNode) string { return fmt.Sprintf("(%s %q)", s.id, s.annotation) },
|
||||||
|
}
|
||||||
|
SpanStackKindAnnotation = &StackKind{
|
||||||
|
symbolizeTask: func(t *traceNode) string { return t.annotation },
|
||||||
|
symbolizeSpan: func(s *traceNode) string { return s.annotation },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n *traceNode) task() *traceNode {
|
||||||
|
task := n.parentTask
|
||||||
|
if n.parentSpan == nil {
|
||||||
|
task = n
|
||||||
|
}
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *traceNode) TaskName() string {
|
||||||
|
task := n.task()
|
||||||
|
return task.annotation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *traceNode) TaskAndSpanStack(kind *StackKind) (spanIdStack string) {
|
||||||
|
task := this.task()
|
||||||
|
|
||||||
|
var spansInTask []*traceNode
|
||||||
|
for s := this; s != nil; s = s.parentSpan {
|
||||||
|
spansInTask = append(spansInTask, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks []*traceNode
|
||||||
|
for t := task; t != nil; t = t.parentTask {
|
||||||
|
tasks = append(tasks, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var taskIdsRev []string
|
||||||
|
for i := len(tasks) - 1; i >= 0; i-- {
|
||||||
|
taskIdsRev = append(taskIdsRev, kind.symbolizeTask(tasks[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
var spanIdsRev []string
|
||||||
|
for i := len(spansInTask) - 1; i >= 0; i-- {
|
||||||
|
spanIdsRev = append(spanIdsRev, kind.symbolizeSpan(spansInTask[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
taskStack := strings.Join(taskIdsRev, "$")
|
||||||
|
return fmt.Sprintf("%s$%s", taskStack, strings.Join(spanIdsRev, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSpanStackOrDefault(ctx context.Context, kind StackKind, def string) string {
|
||||||
|
if nI := ctx.Value(contextKeyTraceNode); nI != nil {
|
||||||
|
n := nI.(*traceNode)
|
||||||
|
return n.TaskAndSpanStack(StackKindId)
|
||||||
|
} else {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpanInfo interface {
|
||||||
|
StartedAt() time.Time
|
||||||
|
EndedAt() time.Time
|
||||||
|
TaskAndSpanStack(kind *StackKind) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Callback struct {
|
||||||
|
OnBegin func(ctx context.Context)
|
||||||
|
OnEnd func(ctx context.Context, spanInfo SpanInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
var callbacks struct {
|
||||||
|
mtx chainlock.L
|
||||||
|
cs []Callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterCallback(c Callback) {
|
||||||
|
callbacks.mtx.HoldWhile(func() {
|
||||||
|
callbacks.cs = append(callbacks.cs, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackBeginSpan(ctx context.Context) func(SpanInfo) {
|
||||||
|
// capture the current state of callbacks into a local variable
|
||||||
|
// this is safe because the slice is append-only and immutable
|
||||||
|
|
||||||
|
// (it is important that a callback registered _after_ callbackBeginSpin is called does not get called on OnEnd)
|
||||||
|
|
||||||
|
var cbs []Callback
|
||||||
|
callbacks.mtx.HoldWhile(func() {
|
||||||
|
cbs = callbacks.cs
|
||||||
|
})
|
||||||
|
for _, cb := range cbs {
|
||||||
|
if cb.OnBegin != nil {
|
||||||
|
cb.OnBegin(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func(spanInfo SpanInfo) {
|
||||||
|
for _, cb := range cbs {
|
||||||
|
if cb.OnEnd != nil {
|
||||||
|
cb.OnEnd(ctx, spanInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
// The functions in this file are concerned with the generation
|
||||||
|
// of trace files based on the information from WithTask and WithSpan.
|
||||||
|
//
|
||||||
|
// The emitted trace files are open-ended array of JSON objects
|
||||||
|
// that follow the Chrome trace file format:
|
||||||
|
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
|
||||||
|
//
|
||||||
|
// The emitted JSON can be loaded into Chrome's chrome://tracing view.
|
||||||
|
//
|
||||||
|
// The trace file can be written to a file whose path is specified in an env file,
|
||||||
|
// and be written to web sockets established on ChrometraceHttpHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
var chrometracePID string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
chrometracePID, err = os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
chrometracePID = fmt.Sprintf("%q", chrometracePID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type chrometraceEvent struct {
|
||||||
|
Cat string `json:"cat,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Stack []string `json:"stack,omitempty"`
|
||||||
|
Phase string `json:"ph"`
|
||||||
|
TimestampUnixMicroseconds int64 `json:"ts"`
|
||||||
|
DurationMicroseconds int64 `json:"dur,omitempty"`
|
||||||
|
Pid string `json:"pid"`
|
||||||
|
Tid string `json:"tid"`
|
||||||
|
Id string `json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func chrometraceBeginSpan(s *traceNode) {
|
||||||
|
taskName := s.TaskName()
|
||||||
|
chrometraceWrite(chrometraceEvent{
|
||||||
|
Name: s.annotation,
|
||||||
|
Phase: "B",
|
||||||
|
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000,
|
||||||
|
Pid: chrometracePID,
|
||||||
|
Tid: taskName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func chrometraceEndSpan(s *traceNode) {
|
||||||
|
taskName := s.TaskName()
|
||||||
|
chrometraceWrite(chrometraceEvent{
|
||||||
|
Name: s.annotation,
|
||||||
|
Phase: "E",
|
||||||
|
TimestampUnixMicroseconds: s.endedAt.UnixNano() / 1000,
|
||||||
|
Pid: chrometracePID,
|
||||||
|
Tid: taskName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var chrometraceFlowId uint64
|
||||||
|
|
||||||
|
func chrometraceBeginTask(s *traceNode) {
|
||||||
|
chrometraceBeginSpan(s)
|
||||||
|
|
||||||
|
if s.parentTask == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// beginning of a task that has a parent
|
||||||
|
// => use flow events to link parent and child
|
||||||
|
|
||||||
|
flowId := atomic.AddUint64(&chrometraceFlowId, 1)
|
||||||
|
flowIdStr := fmt.Sprintf("%x", flowId)
|
||||||
|
|
||||||
|
parentTask := s.parentTask.TaskName()
|
||||||
|
chrometraceWrite(chrometraceEvent{
|
||||||
|
Cat: "task", // seems to be necessary, otherwise the GUI shows some `indexOf` JS error
|
||||||
|
Name: "child-task",
|
||||||
|
Phase: "s",
|
||||||
|
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000, // yes, the child's timestamp (=> from-point of the flow line is at right x-position of parent's bar)
|
||||||
|
Pid: chrometracePID,
|
||||||
|
Tid: parentTask,
|
||||||
|
Id: flowIdStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
childTask := s.TaskName()
|
||||||
|
if parentTask == childTask {
|
||||||
|
panic(parentTask)
|
||||||
|
}
|
||||||
|
chrometraceWrite(chrometraceEvent{
|
||||||
|
Cat: "task", // seems to be necessary, otherwise the GUI shows some `indexOf` JS error
|
||||||
|
Name: "child-task",
|
||||||
|
Phase: "f",
|
||||||
|
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000,
|
||||||
|
Pid: chrometracePID,
|
||||||
|
Tid: childTask,
|
||||||
|
Id: flowIdStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func chrometraceEndTask(s *traceNode) {
|
||||||
|
chrometraceEndSpan(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type chrometraceConsumerRegistration struct {
|
||||||
|
w io.Writer
|
||||||
|
// errored must have capacity 1, the writer thread will send to it non-blocking, then close it
|
||||||
|
errored chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
var chrometraceConsumers struct {
|
||||||
|
register chan chrometraceConsumerRegistration
|
||||||
|
consumers map[chrometraceConsumerRegistration]bool
|
||||||
|
write chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
chrometraceConsumers.register = make(chan chrometraceConsumerRegistration)
|
||||||
|
chrometraceConsumers.consumers = make(map[chrometraceConsumerRegistration]bool)
|
||||||
|
chrometraceConsumers.write = make(chan []byte)
|
||||||
|
go func() {
|
||||||
|
kickConsumer := func(c chrometraceConsumerRegistration, err error) {
|
||||||
|
debug("chrometrace kicking consumer %#v after error %v", c, err)
|
||||||
|
select {
|
||||||
|
case c.errored <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
close(c.errored)
|
||||||
|
delete(chrometraceConsumers.consumers, c)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case reg := <-chrometraceConsumers.register:
|
||||||
|
debug("registered chrometrace consumer %#v", reg)
|
||||||
|
chrometraceConsumers.consumers[reg] = true
|
||||||
|
n, err := reg.w.Write([]byte("[\n"))
|
||||||
|
if err != nil {
|
||||||
|
kickConsumer(reg, err)
|
||||||
|
} else if n != 2 {
|
||||||
|
kickConsumer(reg, fmt.Errorf("short write: %v", n))
|
||||||
|
}
|
||||||
|
// successfully registered
|
||||||
|
|
||||||
|
case buf := <-chrometraceConsumers.write:
|
||||||
|
debug("chrometrace write request: %s", string(buf))
|
||||||
|
var r bytes.Reader
|
||||||
|
for c := range chrometraceConsumers.consumers {
|
||||||
|
r.Reset(buf)
|
||||||
|
n, err := io.Copy(c.w, &r)
|
||||||
|
debug("chrometrace wrote n=%v bytes to consumer %#v", n, c)
|
||||||
|
if err != nil {
|
||||||
|
kickConsumer(c, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func chrometraceWrite(i interface{}) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := json.NewEncoder(&buf).Encode(i)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
buf.WriteString(",")
|
||||||
|
chrometraceConsumers.write <- buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChrometraceClientWebsocketHandler(conn *websocket.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
r := bufio.NewReader(conn)
|
||||||
|
_, _, _ = r.ReadLine() // ignore errors
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
errored := make(chan error, 1)
|
||||||
|
chrometraceConsumers.register <- chrometraceConsumerRegistration{
|
||||||
|
w: conn,
|
||||||
|
errored: errored,
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
<-errored
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
var chrometraceFileConsumerPath = envconst.String("ZREPL_ACTIVITY_TRACE", "")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if chrometraceFileConsumerPath != "" {
|
||||||
|
var err error
|
||||||
|
f, err := os.Create(chrometraceFileConsumerPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
errored := make(chan error, 1)
|
||||||
|
chrometraceConsumers.register <- chrometraceConsumerRegistration{
|
||||||
|
w: f,
|
||||||
|
errored: errored,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
<-errored
|
||||||
|
f.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeyTraceNode contextKey = 1 + iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var contextKeys = []contextKey{
|
||||||
|
contextKeyTraceNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInherit inherits the task hierarchy from inheritFrom into ctx.
|
||||||
|
// The returned context is a child of ctx, but its task and span are those of inheritFrom.
|
||||||
|
//
|
||||||
|
// Note that in most use cases, callers most likely want to call WithTask since it will most likely
|
||||||
|
// be in some sort of connection handler context.
|
||||||
|
func WithInherit(ctx, inheritFrom context.Context) context.Context {
|
||||||
|
for _, k := range contextKeys {
|
||||||
|
if v := inheritFrom.Value(k); v != nil {
|
||||||
|
ctx = context.WithValue(ctx, k, v) // no shadow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// use like this:
|
||||||
|
//
|
||||||
|
// defer WithSpanFromStackUpdateCtx(&existingCtx)()
|
||||||
|
//
|
||||||
|
//
|
||||||
|
func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc {
|
||||||
|
childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic())
|
||||||
|
*ctx = childSpanCtx
|
||||||
|
return end
|
||||||
|
}
|
||||||
|
|
||||||
|
// derive task name from call stack (caller's name)
|
||||||
|
func WithTaskFromStack(ctx context.Context) (context.Context, DoneFunc) {
|
||||||
|
return WithTask(ctx, getMyCallerOrPanic())
|
||||||
|
}
|
||||||
|
|
||||||
|
// derive task name from call stack (caller's name) and update *ctx
|
||||||
|
// to point to be the child task ctx
|
||||||
|
func WithTaskFromStackUpdateCtx(ctx *context.Context) DoneFunc {
|
||||||
|
child, end := WithTask(*ctx, getMyCallerOrPanic())
|
||||||
|
*ctx = child
|
||||||
|
return end
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a task and a span within it in one call
|
||||||
|
func WithTaskAndSpan(ctx context.Context, task string, span string) (context.Context, DoneFunc) {
|
||||||
|
ctx, endTask := WithTask(ctx, task)
|
||||||
|
ctx, endSpan := WithSpan(ctx, fmt.Sprintf("%s %s", task, span))
|
||||||
|
return ctx, func() {
|
||||||
|
endSpan()
|
||||||
|
endTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a span during which several child tasks are spawned using the `add` function
|
||||||
|
//
|
||||||
|
// IMPORTANT FOR USERS: Caller must ensure that the capturing behavior is correct, the Go linter doesn't catch this.
|
||||||
|
func WithTaskGroup(ctx context.Context, taskGroup string) (_ context.Context, add func(f func(context.Context)), waitEnd DoneFunc) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
ctx, endSpan := WithSpan(ctx, taskGroup)
|
||||||
|
add = func(f func(context.Context)) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
ctx, endTask := WithTask(ctx, taskGroup)
|
||||||
|
defer endTask()
|
||||||
|
f(ctx)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
waitEnd = func() {
|
||||||
|
wg.Wait()
|
||||||
|
endSpan()
|
||||||
|
}
|
||||||
|
return ctx, add, waitEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMyCallerOrPanic() string {
|
||||||
|
pc, _, _, ok := runtime.Caller(2)
|
||||||
|
if !ok {
|
||||||
|
panic("cannot get caller")
|
||||||
|
}
|
||||||
|
details := runtime.FuncForPC(pc)
|
||||||
|
if ok && details != nil {
|
||||||
|
const prefix = "github.com/zrepl/zrepl"
|
||||||
|
return strings.TrimPrefix(strings.TrimPrefix(details.Name(), prefix), "/")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCallerOrPanic(t *testing.T) {
|
||||||
|
withStackFromCtxMock := func() string {
|
||||||
|
return getMyCallerOrPanic()
|
||||||
|
}
|
||||||
|
ret := withStackFromCtxMock()
|
||||||
|
// zrepl prefix is stripped
|
||||||
|
assert.Equal(t, "daemon/logging/trace.TestGetCallerOrPanic", ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithTaskGroupRunTasksConcurrently(t *testing.T) {
|
||||||
|
|
||||||
|
// spawn a task group where each task waits for the other to start
|
||||||
|
// => without concurrency, they would hang
|
||||||
|
|
||||||
|
rootCtx, endRoot := WithTaskFromStack(context.Background())
|
||||||
|
defer endRoot()
|
||||||
|
|
||||||
|
_, add, waitEnd := WithTaskGroup(rootCtx, "test-task-group")
|
||||||
|
|
||||||
|
schedulerTimeout := 2 * time.Second
|
||||||
|
timeout := time.After(schedulerTimeout)
|
||||||
|
var hadTimeout uint32
|
||||||
|
started0, started1 := make(chan struct{}), make(chan struct{})
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
i := i // capture by copy
|
||||||
|
add(func(ctx context.Context) {
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
close(started0)
|
||||||
|
select {
|
||||||
|
case <-started1:
|
||||||
|
case <-timeout:
|
||||||
|
atomic.AddUint32(&hadTimeout, 1)
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
close(started1)
|
||||||
|
select {
|
||||||
|
case <-started0:
|
||||||
|
case <-timeout:
|
||||||
|
atomic.AddUint32(&hadTimeout, 1)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
waitEnd()
|
||||||
|
assert.Zero(t, hadTimeout, "either bad impl or scheduler timeout (which is %v)", schedulerTimeout)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debugEnabled = envconst.Bool("ZREPL_TRACE_DEBUG_ENABLED", false)
|
||||||
|
|
||||||
|
func debug(format string, args ...interface{}) {
|
||||||
|
if !debugEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
var genIdNumBytes = envconst.Int("ZREPL_TRACE_ID_NUM_BYTES", 3)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if genIdNumBytes < 1 {
|
||||||
|
panic("trace node id byte length must be at least 1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func genID() string {
|
||||||
|
var out strings.Builder
|
||||||
|
enc := base64.NewEncoder(base64.RawStdEncoding, &out)
|
||||||
|
buf := make([]byte, genIdNumBytes)
|
||||||
|
for i := 0; i < len(buf); {
|
||||||
|
n, err := rand.Read(buf[i:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
i += n
|
||||||
|
}
|
||||||
|
n, err := enc.Write(buf[:])
|
||||||
|
if err != nil || n != len(buf) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := enc.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gitchander/permutation"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegularSpanUsage(t *testing.T) {
|
||||||
|
root, endRoot := WithTask(context.Background(), "root")
|
||||||
|
defer endRoot()
|
||||||
|
|
||||||
|
s1, endS1 := WithSpan(root, "parent")
|
||||||
|
s2, endS2 := WithSpan(s1, "child")
|
||||||
|
_, endS3 := WithSpan(s2, "grand-child")
|
||||||
|
require.NotPanics(t, func() { endS3() })
|
||||||
|
require.NotPanics(t, func() { endS2() })
|
||||||
|
|
||||||
|
// reuse
|
||||||
|
_, endS4 := WithSpan(s1, "child-2")
|
||||||
|
require.NotPanics(t, func() { endS4() })
|
||||||
|
|
||||||
|
// close parent
|
||||||
|
require.NotPanics(t, func() { endS1() })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipleActiveChildSpansNotAllowed(t *testing.T) {
|
||||||
|
root, endRoot := WithTask(context.Background(), "root")
|
||||||
|
defer endRoot()
|
||||||
|
|
||||||
|
s1, _ := WithSpan(root, "s1")
|
||||||
|
_, endS2 := WithSpan(s1, "s1-child1")
|
||||||
|
|
||||||
|
require.PanicsWithValue(t, ErrAlreadyActiveChildSpan, func() {
|
||||||
|
_, _ = WithSpan(s1, "s1-child2")
|
||||||
|
})
|
||||||
|
|
||||||
|
endS2()
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
_, _ = WithSpan(s1, "s1-child2")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForkingChildSpansNotAllowed(t *testing.T) {
|
||||||
|
root, endRoot := WithTask(context.Background(), "root")
|
||||||
|
defer endRoot()
|
||||||
|
|
||||||
|
s1, _ := WithSpan(root, "s1")
|
||||||
|
sc, endSC := WithSpan(s1, "s1-child")
|
||||||
|
_, _ = WithSpan(sc, "s1-child-child")
|
||||||
|
|
||||||
|
require.PanicsWithValue(t, ErrSpanStillHasActiveChildSpan, func() {
|
||||||
|
endSC()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegularTaskUsage(t *testing.T) {
|
||||||
|
// assert concurrent activities on different tasks can end in any order
|
||||||
|
closeOrder := []int{0, 1, 2}
|
||||||
|
closeOrders := permutation.New(permutation.IntSlice(closeOrder))
|
||||||
|
for closeOrders.Next() {
|
||||||
|
t.Run(fmt.Sprintf("%v", closeOrder), func(t *testing.T) {
|
||||||
|
root, endRoot := WithTask(context.Background(), "root")
|
||||||
|
defer endRoot()
|
||||||
|
|
||||||
|
c1, endC1 := WithTask(root, "c1")
|
||||||
|
defer endC1()
|
||||||
|
c2, endC2 := WithTask(root, "c2")
|
||||||
|
defer endC2()
|
||||||
|
|
||||||
|
// begin 3 concurrent activities
|
||||||
|
_, endAR := WithSpan(root, "aR")
|
||||||
|
_, endAC1 := WithSpan(c1, "aC1")
|
||||||
|
_, endAC2 := WithSpan(c2, "aC2")
|
||||||
|
|
||||||
|
endFuncs := []DoneFunc{endAR, endAC1, endAC2}
|
||||||
|
for _, i := range closeOrder {
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
endFuncs[i]()
|
||||||
|
}, "%v", i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskEndWithActiveChildTaskNotAllowed(t *testing.T) {
|
||||||
|
root, _ := WithTask(context.Background(), "root")
|
||||||
|
c, endC := WithTask(root, "child")
|
||||||
|
_, _ = WithTask(c, "grand-child")
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
require.NotNil(t, r)
|
||||||
|
err, ok := r.(error)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, ErrTaskStillHasActiveChildTasks, errors.Cause(err))
|
||||||
|
}()
|
||||||
|
endC()
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdempotentEndTask(t *testing.T) {
|
||||||
|
_, end := WithTask(context.Background(), "root")
|
||||||
|
end()
|
||||||
|
require.NotPanics(t, func() { end() })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpansPanicIfNoParentTask(t *testing.T) {
|
||||||
|
require.Panics(t, func() { WithSpan(context.Background(), "taskless-span") })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdempotentEndSpan(t *testing.T) {
|
||||||
|
root, _ := WithTask(context.Background(), "root")
|
||||||
|
_, end := WithSpan(root, "span")
|
||||||
|
end()
|
||||||
|
require.NotPanics(t, func() { end() })
|
||||||
|
}
|
||||||
|
|
||||||
|
func logAndGetTraceNode(t *testing.T, descr string, ctx context.Context) *traceNode {
|
||||||
|
n, ok := ctx.Value(contextKeyTraceNode).(*traceNode)
|
||||||
|
require.True(t, ok)
|
||||||
|
t.Logf("% 20s %p %#v", descr, n, n)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhiteboxHierachy(t *testing.T) {
|
||||||
|
root, e1 := WithTask(context.Background(), "root")
|
||||||
|
rootN := logAndGetTraceNode(t, "root", root)
|
||||||
|
assert.Nil(t, rootN.parentTask)
|
||||||
|
assert.Nil(t, rootN.parentSpan)
|
||||||
|
|
||||||
|
child, e2 := WithSpan(root, "child")
|
||||||
|
childN := logAndGetTraceNode(t, "child", child)
|
||||||
|
assert.Equal(t, rootN, childN.parentTask)
|
||||||
|
assert.Equal(t, rootN, childN.parentSpan)
|
||||||
|
|
||||||
|
grandchild, e3 := WithSpan(child, "grandchild")
|
||||||
|
grandchildN := logAndGetTraceNode(t, "grandchild", grandchild)
|
||||||
|
assert.Equal(t, rootN, grandchildN.parentTask)
|
||||||
|
assert.Equal(t, childN, grandchildN.parentSpan)
|
||||||
|
|
||||||
|
gcTask, e4 := WithTask(grandchild, "grandchild-task")
|
||||||
|
gcTaskN := logAndGetTraceNode(t, "grandchild-task", gcTask)
|
||||||
|
assert.Equal(t, rootN, gcTaskN.parentTask)
|
||||||
|
assert.Nil(t, gcTaskN.parentSpan)
|
||||||
|
|
||||||
|
// it is allowed that a child task outlives the _span_ in which it was created
|
||||||
|
// (albeit not its parent task)
|
||||||
|
e3()
|
||||||
|
e2()
|
||||||
|
gcTaskSpan, e5 := WithSpan(gcTask, "granschild-task-span")
|
||||||
|
gcTaskSpanN := logAndGetTraceNode(t, "granschild-task-span", gcTaskSpan)
|
||||||
|
assert.Equal(t, gcTaskN, gcTaskSpanN.parentTask)
|
||||||
|
assert.Equal(t, gcTaskN, gcTaskSpanN.parentSpan)
|
||||||
|
e5()
|
||||||
|
|
||||||
|
e4()
|
||||||
|
e1()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrphanTasksBecomeNewRootWhenAllAncestorsAreDead(t *testing.T) {
|
||||||
|
parent, e1 := WithTask(context.Background(), "parent")
|
||||||
|
_ = logAndGetTraceNode(t, "parent-task", parent)
|
||||||
|
e1()
|
||||||
|
|
||||||
|
var child context.Context
|
||||||
|
var e2 DoneFunc
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
child, e2 = WithTask(parent, "child")
|
||||||
|
})
|
||||||
|
childN := logAndGetTraceNode(t, "child-task", child)
|
||||||
|
assert.Nil(t, childN.parentTask)
|
||||||
|
|
||||||
|
grandchild, _ := WithTask(child, "grandchild")
|
||||||
|
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
|
||||||
|
assert.Equal(t, childN, grandchildN.parentTask)
|
||||||
|
|
||||||
|
require.Panics(t, func() { e2() }, "if the parent was alive at creation of child task, wrong termination order remains a panicable offense though")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrphanTaskBecomesChildToNearestLiveAncestor(t *testing.T) {
|
||||||
|
parent, e1 := WithTask(context.Background(), "parent")
|
||||||
|
parentN := logAndGetTraceNode(t, "parent-task", parent)
|
||||||
|
|
||||||
|
child, e2 := WithTask(parent, "child")
|
||||||
|
childN := logAndGetTraceNode(t, "child-task", child)
|
||||||
|
assert.Equal(t, parentN, childN.parentTask)
|
||||||
|
e2()
|
||||||
|
|
||||||
|
grandchild, e3 := WithTask(child, "grandchild")
|
||||||
|
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
|
||||||
|
assert.Equal(t, parentN, grandchildN.parentTask) // child is already dead
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
e3()
|
||||||
|
e1()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/willf/bitset"
|
||||||
|
)
|
||||||
|
|
||||||
|
type uniqueConcurrentTaskNamer struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
active map[string]*bitset.BitSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitvecLengthGauge may be nil
|
||||||
|
func newUniqueTaskNamer() *uniqueConcurrentTaskNamer {
|
||||||
|
return &uniqueConcurrentTaskNamer{
|
||||||
|
active: make(map[string]*bitset.BitSet),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// appends `#%d` to `name` such that until `done` is called,
|
||||||
|
// it is guaranteed that `#%d` is not returned a second time for the same `name`
|
||||||
|
func (namer *uniqueConcurrentTaskNamer) UniqueConcurrentTaskName(name string) (uniqueName string, done func()) {
|
||||||
|
if strings.Contains(name, "#") {
|
||||||
|
panic(name)
|
||||||
|
}
|
||||||
|
namer.mtx.Lock()
|
||||||
|
act, ok := namer.active[name]
|
||||||
|
if !ok {
|
||||||
|
act = bitset.New(64) // FIXME magic const
|
||||||
|
namer.active[name] = act
|
||||||
|
}
|
||||||
|
id, ok := act.NextClear(0)
|
||||||
|
if !ok {
|
||||||
|
// if !ok, all bits are 1 and act.Len() returns the next bit
|
||||||
|
id = act.Len()
|
||||||
|
// FIXME unbounded growth without reclamation
|
||||||
|
}
|
||||||
|
act.Set(id)
|
||||||
|
namer.mtx.Unlock()
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s#%d", name, id), func() {
|
||||||
|
namer.mtx.Lock()
|
||||||
|
defer namer.mtx.Unlock()
|
||||||
|
act, ok := namer.active[name]
|
||||||
|
if !ok {
|
||||||
|
panic("must be initialized upon entry")
|
||||||
|
}
|
||||||
|
act.Clear(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/willf/bitset"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBitsetFeaturesForUniqueConcurrentTaskNamer(t *testing.T) {
|
||||||
|
var b bitset.BitSet
|
||||||
|
require.Equal(t, uint(0), b.Len())
|
||||||
|
require.Equal(t, uint(0), b.Count())
|
||||||
|
|
||||||
|
b.Set(0)
|
||||||
|
require.Equal(t, uint(1), b.Len())
|
||||||
|
require.Equal(t, uint(1), b.Count())
|
||||||
|
|
||||||
|
b.Set(8)
|
||||||
|
require.Equal(t, uint(9), b.Len())
|
||||||
|
require.Equal(t, uint(2), b.Count())
|
||||||
|
|
||||||
|
b.Set(1)
|
||||||
|
require.Equal(t, uint(9), b.Len())
|
||||||
|
require.Equal(t, uint(3), b.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueConcurrentTaskNamer(t *testing.T) {
|
||||||
|
namer := newUniqueTaskNamer()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
const N = 8128
|
||||||
|
const Q = 23
|
||||||
|
|
||||||
|
var fails uint32
|
||||||
|
var m sync.Map
|
||||||
|
wg.Add(N)
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
name := fmt.Sprintf("%d", i/Q)
|
||||||
|
uniqueName, done := namer.UniqueConcurrentTaskName(name)
|
||||||
|
act, _ := m.LoadOrStore(uniqueName, i)
|
||||||
|
if act.(int) != i {
|
||||||
|
atomic.AddUint32(&fails, 1)
|
||||||
|
}
|
||||||
|
m.Delete(uniqueName)
|
||||||
|
done()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
require.Equal(t, uint32(0), fails)
|
||||||
|
}
|
||||||
+4
-2
@@ -1,6 +1,8 @@
|
|||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
)
|
)
|
||||||
@@ -10,7 +12,7 @@ type Logger = logger.Logger
|
|||||||
var DaemonCmd = &cli.Subcommand{
|
var DaemonCmd = &cli.Subcommand{
|
||||||
Use: "daemon",
|
Use: "daemon",
|
||||||
Short: "run the zrepl daemon",
|
Short: "run the zrepl daemon",
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
return Run(subcommand.Config())
|
return Run(ctx, subcommand.Config())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pprofServer struct {
|
type pprofServer struct {
|
||||||
@@ -64,6 +68,8 @@ outer:
|
|||||||
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
||||||
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
||||||
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
|
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
|
||||||
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
|
mux.Handle("/debug/zrepl/activity-trace", websocket.Handler(trace.ChrometraceClientWebsocketHandler))
|
||||||
go func() {
|
go func() {
|
||||||
err := http.Serve(s.listener, mux)
|
err := http.Serve(s.listener, mux)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
|
|||||||
+16
-7
@@ -10,20 +10,24 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
||||||
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type prometheusJob struct {
|
type prometheusJob struct {
|
||||||
listen string
|
listen string
|
||||||
|
freeBind bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
||||||
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &prometheusJob{in.Listen}, nil
|
return &prometheusJob{in.Listen, in.ListenFreeBind}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var prom struct {
|
var prom struct {
|
||||||
@@ -46,6 +50,8 @@ func (j *prometheusJob) Status() *job.Status { return &job.Status{Type: job.Type
|
|||||||
|
|
||||||
func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||||
|
|
||||||
|
func (j *prometheusJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
|
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||||
|
|
||||||
func (j *prometheusJob) Run(ctx context.Context) {
|
func (j *prometheusJob) Run(ctx context.Context) {
|
||||||
@@ -60,7 +66,7 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
|||||||
|
|
||||||
log := job.GetLogger(ctx)
|
log := job.GetLogger(ctx)
|
||||||
|
|
||||||
l, err := net.Listen("tcp", j.listen)
|
l, err := tcpsock.Listen(j.listen, j.freeBind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("cannot listen")
|
log.WithError(err).Error("cannot listen")
|
||||||
return
|
return
|
||||||
@@ -81,16 +87,19 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type prometheusJobOutlet struct {
|
type prometheusJobOutlet struct {
|
||||||
jobName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ logger.Outlet = prometheusJobOutlet{}
|
var _ logger.Outlet = prometheusJobOutlet{}
|
||||||
|
|
||||||
func newPrometheusLogOutlet(jobName string) prometheusJobOutlet {
|
func newPrometheusLogOutlet() prometheusJobOutlet {
|
||||||
return prometheusJobOutlet{jobName}
|
return prometheusJobOutlet{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
||||||
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
|
jobFieldVal, ok := entry.Fields[logging.JobField].(string)
|
||||||
|
if !ok {
|
||||||
|
jobFieldVal = "_nojobid"
|
||||||
|
}
|
||||||
|
prom.taskLogEntries.WithLabelValues(jobFieldVal, entry.Level.String()).Inc()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-29
@@ -12,19 +12,28 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/pruning"
|
"github.com/zrepl/zrepl/pruning"
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
// The sender in the replication setup.
|
||||||
type History interface {
|
// The pruner uses the Sender to determine which of the Target's filesystems need to be pruned.
|
||||||
|
// Also, it asks the Sender about the replication cursor of each filesystem
|
||||||
|
// to enable the 'not_replicated' pruning rule.
|
||||||
|
//
|
||||||
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
|
type Sender interface {
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
// The pruning target, i.e., on which snapshots are destroyed.
|
||||||
|
// This can be a replication sender or receiver.
|
||||||
|
//
|
||||||
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type Target interface {
|
type Target interface {
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||||
@@ -35,23 +44,19 @@ type Logger = logger.Logger
|
|||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
|
|
||||||
const contextKeyLogger contextKey = 0
|
const (
|
||||||
|
contextKeyPruneSide contextKey = 1 + iota
|
||||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
)
|
||||||
return context.WithValue(ctx, contextKeyLogger, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogger(ctx context.Context) Logger {
|
func GetLogger(ctx context.Context) Logger {
|
||||||
if l, ok := ctx.Value(contextKeyLogger).(Logger); ok {
|
pruneSide := ctx.Value(contextKeyPruneSide).(string)
|
||||||
return l
|
return logging.GetLogger(ctx, logging.SubsysPruning).WithField("prune_side", pruneSide)
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
target Target
|
target Target
|
||||||
receiver History
|
sender Sender
|
||||||
rules []pruning.KeepRule
|
rules []pruning.KeepRule
|
||||||
retryWait time.Duration
|
retryWait time.Duration
|
||||||
considerSnapAtCursorReplicated bool
|
considerSnapAtCursorReplicated bool
|
||||||
@@ -135,12 +140,12 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "sender")),
|
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
sender,
|
||||||
f.senderRules,
|
f.senderRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
f.considerSnapAtCursorReplicated,
|
f.considerSnapAtCursorReplicated,
|
||||||
@@ -151,12 +156,12 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "receiver")),
|
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
sender,
|
||||||
f.receiverRules,
|
f.receiverRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // senseless here anyways
|
false, // senseless here anyways
|
||||||
@@ -167,12 +172,12 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, history Sender) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
ctx,
|
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
history,
|
||||||
f.keepRules,
|
f.keepRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
||||||
@@ -346,9 +351,9 @@ func (s snapshot) Date() time.Time { return s.date }
|
|||||||
|
|
||||||
func doOneAttempt(a *args, u updater) {
|
func doOneAttempt(a *args, u updater) {
|
||||||
|
|
||||||
ctx, target, receiver := a.ctx, a.target, a.receiver
|
ctx, target, sender := a.ctx, a.target, a.sender
|
||||||
|
|
||||||
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
sfssres, err := sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u(func(p *Pruner) {
|
u(func(p *Pruner) {
|
||||||
p.state = PlanErr
|
p.state = PlanErr
|
||||||
@@ -412,11 +417,8 @@ tfss_loop:
|
|||||||
|
|
||||||
rcReq := &pdu.ReplicationCursorReq{
|
rcReq := &pdu.ReplicationCursorReq{
|
||||||
Filesystem: tfs.Path,
|
Filesystem: tfs.Path,
|
||||||
Op: &pdu.ReplicationCursorReq_Get{
|
|
||||||
Get: &pdu.ReplicationCursorReq_GetOp{},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
rc, err := sender.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
@@ -462,7 +464,7 @@ tfss_loop:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if preCursor {
|
if preCursor {
|
||||||
pfsPlanErrAndLog(fmt.Errorf("replication cursor not found in prune target filesystem versions"), "")
|
pfsPlanErrAndLog(fmt.Errorf("prune target has no snapshot that corresponds to sender replication cursor bookmark"), "")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+108
-74
@@ -9,10 +9,13 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,10 +47,9 @@ type snapProgress struct {
|
|||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
log Logger
|
|
||||||
prefix string
|
prefix string
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
fsf *filters.DatasetMapFilter
|
fsf zfs.DatasetFilter
|
||||||
snapshotsTaken chan<- struct{}
|
snapshotsTaken chan<- struct{}
|
||||||
hooks *hooks.List
|
hooks *hooks.List
|
||||||
dryRun bool
|
dryRun bool
|
||||||
@@ -101,26 +103,13 @@ func (s State) sf() state {
|
|||||||
type updater func(u func(*Snapper)) State
|
type updater func(u func(*Snapper)) State
|
||||||
type state func(a args, u updater) state
|
type state func(a args, u updater) state
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyLog contextKey = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
type Logger = logger.Logger
|
||||||
|
|
||||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyLog, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
func getLogger(ctx context.Context) Logger {
|
||||||
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
||||||
return log
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
func PeriodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
||||||
if in.Prefix == "" {
|
if in.Prefix == "" {
|
||||||
return nil, errors.New("prefix must not be empty")
|
return nil, errors.New("prefix must not be empty")
|
||||||
}
|
}
|
||||||
@@ -145,13 +134,12 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
getLogger(ctx).Debug("start")
|
getLogger(ctx).Debug("start")
|
||||||
defer getLogger(ctx).Debug("stop")
|
defer getLogger(ctx).Debug("stop")
|
||||||
|
|
||||||
s.args.snapshotsTaken = snapshotsTaken
|
s.args.snapshotsTaken = snapshotsTaken
|
||||||
s.args.ctx = ctx
|
s.args.ctx = ctx
|
||||||
s.args.log = getLogger(ctx)
|
|
||||||
s.args.dryRun = false // for future expansion
|
s.args.dryRun = false // for future expansion
|
||||||
|
|
||||||
u := func(u func(*Snapper)) State {
|
u := func(u func(*Snapper)) State {
|
||||||
@@ -189,7 +177,7 @@ func onErr(err error, u updater) state {
|
|||||||
case Snapshotting:
|
case Snapshotting:
|
||||||
s.state = ErrorWait
|
s.state = ErrorWait
|
||||||
}
|
}
|
||||||
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
||||||
}).sf()
|
}).sf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +196,7 @@ func syncUp(a args, u updater) state {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
syncPoint, err := findSyncPoint(a.log, fss, a.prefix, a.interval)
|
syncPoint, err := findSyncPoint(a.ctx, fss, a.prefix, a.interval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
@@ -265,18 +253,18 @@ func snapshot(a args, u updater) state {
|
|||||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||||
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
||||||
|
|
||||||
l := a.log.
|
ctx := logging.WithInjectedField(a.ctx, "fs", fs.ToString())
|
||||||
WithField("fs", fs.ToString()).
|
ctx = logging.WithInjectedField(ctx, "snap", snapname)
|
||||||
WithField("snap", snapname)
|
|
||||||
|
|
||||||
hookEnvExtra := hooks.Env{
|
hookEnvExtra := hooks.Env{
|
||||||
hooks.EnvFS: fs.ToString(),
|
hooks.EnvFS: fs.ToString(),
|
||||||
hooks.EnvSnapshot: snapname,
|
hooks.EnvSnapshot: snapname,
|
||||||
}
|
}
|
||||||
|
|
||||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
||||||
|
l := getLogger(ctx)
|
||||||
l.Debug("create snapshot")
|
l.Debug("create snapshot")
|
||||||
err = zfs.ZFSSnapshot(fs, snapname, false) // TODO propagagte context to ZFSSnapshot
|
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
l.WithError(err).Error("cannot create snapshot")
|
||||||
}
|
}
|
||||||
@@ -289,7 +277,7 @@ func snapshot(a args, u updater) state {
|
|||||||
{
|
{
|
||||||
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("unexpected filter error")
|
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
||||||
fsHadErr = true
|
fsHadErr = true
|
||||||
goto updateFSState
|
goto updateFSState
|
||||||
}
|
}
|
||||||
@@ -302,7 +290,7 @@ func snapshot(a args, u updater) state {
|
|||||||
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
||||||
if planErr != nil {
|
if planErr != nil {
|
||||||
fsHadErr = true
|
fsHadErr = true
|
||||||
l.WithError(planErr).Error("cannot create job hook plan")
|
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
||||||
goto updateFSState
|
goto updateFSState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,15 +301,14 @@ func snapshot(a args, u updater) state {
|
|||||||
progress.state = SnapStarted
|
progress.state = SnapStarted
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
|
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
|
||||||
l.WithField("report", plan.Report().String()).Debug("begin run job plan")
|
plan.Run(ctx, a.dryRun)
|
||||||
plan.Run(hooks.WithLogger(a.ctx, l), a.dryRun)
|
|
||||||
planReport = plan.Report()
|
planReport = plan.Report()
|
||||||
fsHadErr = planReport.HadError() // not just fatal errors
|
fsHadErr = planReport.HadError() // not just fatal errors
|
||||||
if fsHadErr {
|
if fsHadErr {
|
||||||
l.WithField("report", planReport.String()).Error("end run job plan with error")
|
getLogger(ctx).WithField("report", planReport.String()).Error("end run job plan with error")
|
||||||
} else {
|
} else {
|
||||||
l.WithField("report", planReport.String()).Info("end run job plan successful")
|
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +328,7 @@ func snapshot(a args, u updater) state {
|
|||||||
case a.snapshotsTaken <- struct{}{}:
|
case a.snapshotsTaken <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
if a.snapshotsTaken != nil {
|
if a.snapshotsTaken != nil {
|
||||||
a.log.Warn("callback channel is full, discarding snapshot update event")
|
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +341,7 @@ func snapshot(a args, u updater) state {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.log.WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +362,7 @@ func wait(a args, u updater) state {
|
|||||||
lastTick := snapper.lastInvocation
|
lastTick := snapper.lastInvocation
|
||||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
snapper.sleepUntil = lastTick.Add(a.interval)
|
||||||
sleepUntil = snapper.sleepUntil
|
sleepUntil = snapper.sleepUntil
|
||||||
log := a.log.WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
||||||
logFunc := log.Debug
|
logFunc := log.Debug
|
||||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
||||||
logFunc = log.Error
|
logFunc = log.Error
|
||||||
@@ -396,13 +383,23 @@ func wait(a args, u updater) state {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
|
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
||||||
return zfs.ZFSListMapping(ctx, mf)
|
return zfs.ZFSListMapping(ctx, mf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
||||||
|
|
||||||
|
// see docs/snapshotting.rst
|
||||||
|
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
||||||
|
|
||||||
|
const (
|
||||||
|
prioHasVersions int = iota
|
||||||
|
prioNoVersions
|
||||||
|
)
|
||||||
|
|
||||||
type snapTime struct {
|
type snapTime struct {
|
||||||
ds *zfs.DatasetPath
|
ds *zfs.DatasetPath
|
||||||
|
prio int // lower is higher
|
||||||
time time.Time
|
time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,55 +408,92 @@ func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval t
|
|||||||
}
|
}
|
||||||
|
|
||||||
snaptimes := make([]snapTime, 0, len(fss))
|
snaptimes := make([]snapTime, 0, len(fss))
|
||||||
|
hardErrs := 0
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
log.Debug("examine filesystem state")
|
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
||||||
for _, d := range fss {
|
for _, d := range fss {
|
||||||
|
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
||||||
l := log.WithField("fs", d.ToString())
|
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
||||||
|
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
snaptimes = append(snaptimes, snapTime{
|
||||||
if err != nil {
|
ds: d,
|
||||||
l.WithError(err).Error("cannot list filesystem versions")
|
prio: prioNoVersions,
|
||||||
continue
|
time: now,
|
||||||
}
|
})
|
||||||
if len(fsvs) <= 0 {
|
} else if err != nil {
|
||||||
l.WithField("prefix", prefix).Debug("no filesystem versions with prefix")
|
hardErrs++
|
||||||
continue
|
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
||||||
|
} else {
|
||||||
|
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
||||||
|
snaptimes = append(snaptimes, snapTime{
|
||||||
|
ds: d,
|
||||||
|
prio: prioHasVersions,
|
||||||
|
time: syncPoint,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort versions by creation
|
if hardErrs == len(fss) {
|
||||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
||||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
|
||||||
})
|
|
||||||
|
|
||||||
latest := fsvs[len(fsvs)-1]
|
|
||||||
l.WithField("creation", latest.Creation).
|
|
||||||
Debug("found latest snapshot")
|
|
||||||
|
|
||||||
since := now.Sub(latest.Creation)
|
|
||||||
if since < 0 {
|
|
||||||
l.WithField("snapshot", latest.Name).
|
|
||||||
WithField("creation", latest.Creation).
|
|
||||||
Error("snapshot is from the future")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
next := now
|
|
||||||
if since < interval {
|
|
||||||
next = latest.Creation.Add(interval)
|
|
||||||
}
|
|
||||||
snaptimes = append(snaptimes, snapTime{d, next})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(snaptimes) == 0 {
|
if len(snaptimes) == 0 {
|
||||||
snaptimes = append(snaptimes, snapTime{nil, now})
|
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sort ascending by (prio,time)
|
||||||
|
// => those filesystems with versions win over those without any
|
||||||
sort.Slice(snaptimes, func(i, j int) bool {
|
sort.Slice(snaptimes, func(i, j int) bool {
|
||||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
if snaptimes[i].prio == snaptimes[j].prio {
|
||||||
|
return snaptimes[i].time.Before(snaptimes[j].time)
|
||||||
|
}
|
||||||
|
return snaptimes[i].prio < snaptimes[j].prio
|
||||||
})
|
})
|
||||||
|
|
||||||
|
winnerSyncPoint := snaptimes[0].time
|
||||||
|
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
|
||||||
|
l.Info("determined sync point")
|
||||||
|
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
||||||
|
for _, st := range snaptimes {
|
||||||
|
if st.prio == prioNoVersions {
|
||||||
|
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return snaptimes[0].time, nil
|
return snaptimes[0].time, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
||||||
|
|
||||||
|
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||||
|
|
||||||
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
|
||||||
|
Types: zfs.Snapshots,
|
||||||
|
ShortnamePrefix: prefix,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||||
|
}
|
||||||
|
if len(fsvs) <= 0 {
|
||||||
|
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort versions by creation
|
||||||
|
sort.SliceStable(fsvs, func(i, j int) bool {
|
||||||
|
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||||
|
})
|
||||||
|
|
||||||
|
latest := fsvs[len(fsvs)-1]
|
||||||
|
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
|
||||||
|
|
||||||
|
since := now.Sub(latest.Creation)
|
||||||
|
if since < 0 {
|
||||||
|
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest.Creation.Add(interval), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FIXME: properly abstract snapshotting:
|
// FIXME: properly abstract snapshotting:
|
||||||
@@ -32,7 +32,7 @@ func (s *PeriodicOrManual) Report() *Report {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SnapshottingPeriodic:
|
case *config.SnapshottingPeriodic:
|
||||||
snapper, err := PeriodicFromConfig(g, fsf, v)
|
snapper, err := PeriodicFromConfig(g, fsf, v)
|
||||||
|
|||||||
-595
@@ -1,595 +0,0 @@
|
|||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"$$hashKey": "object:3351",
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": "-- Grafana --",
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"gnetId": null,
|
|
||||||
"graphTooltip": 1,
|
|
||||||
"id": 7,
|
|
||||||
"iteration": 1552746418826,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"content": "# zrepl Prometheus Metrics\n\nzrepl exposes Prometheus metrics and ships with this Grafana dashboard.\nThe exported metrics are suitable for health checks:\n\n* The log should generally be warning & error-free\n * The `Log Messages that require attention` graph visualizes log message counts indicating problems.\n* The number of goroutines should not grow unboundedly over time.\n * During replication, the number of goroutines can be way higher than during idle time.\n * If the goroutine count grows with each replication, there is clearly a goroutine leak. Please open a bug report.\n* The sys memory consumption should not grow unboundedly over time.\n * Note that the Go runtime pre-allocates some of its heap from the OS.\n * zrepl actually uses much less memory than allocated from the OS.\n * Since Go 1.11, Go pre-allocates more aggressively.\n* Monitor that some data is replicated, although that metric does not guarantee that replication was successful.\n\n**In general, note that the exported metrics are not stable unless declared otherwise.**",
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 35,
|
|
||||||
"mode": "markdown",
|
|
||||||
"title": "Panel Title",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 9
|
|
||||||
},
|
|
||||||
"id": 15,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": true,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"$$hashKey": "object:3436",
|
|
||||||
"expr": "up{job='$prom_job_name'}",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "zrepl Instances Up",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": "5",
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 9
|
|
||||||
},
|
|
||||||
"id": 22,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "increase(zrepl_daemon_log_entries{job='$prom_job_name',level=~'warn|error'}[$__interval])",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Log Messages that require attention",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 18
|
|
||||||
},
|
|
||||||
"id": 33,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [
|
|
||||||
{
|
|
||||||
"alias": "/replicated bytes in last.*/",
|
|
||||||
"yaxis": 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "sum(rate(zrepl_replication_bytes_replicated{job='$prom_job_name'}[$__interval])) by (zrepl_job)",
|
|
||||||
"format": "time_series",
|
|
||||||
"hide": false,
|
|
||||||
"interval": "",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"legendFormat": "replication data rate zrepl_job={{zrepl_job}}",
|
|
||||||
"refId": "A"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expr": "sum(increase(zrepl_replication_bytes_replicated{job='$prom_job_name'}[10m])) by (zrepl_job)",
|
|
||||||
"format": "time_series",
|
|
||||||
"hide": false,
|
|
||||||
"interval": "",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"legendFormat": "replicated bytes in last 10min zrepl_job={{zrepl_job}}",
|
|
||||||
"refId": "B"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Replication Data Rate and Volume(integrates last 10min)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "Bps",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "bytes",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 18
|
|
||||||
},
|
|
||||||
"id": 23,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "sum(increase(zrepl_daemon_log_entries{job='$prom_job_name',zrepl_job=~\"^[^_].*\"}[$__interval])) by (instance,zrepl_job)",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Log Activity (without internal jobs)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 27
|
|
||||||
},
|
|
||||||
"id": 17,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"$$hashKey": "object:3535",
|
|
||||||
"expr": "go_memstats_sys_bytes{job='$prom_job_name'}",
|
|
||||||
"format": "time_series",
|
|
||||||
"hide": false,
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Memory Allocated by the Go runtime from the OS (should not grow unboundedly)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "bytes",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 27
|
|
||||||
},
|
|
||||||
"id": 19,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "go_goroutines{job='$prom_job_name'}",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "number of goroutines (should not grow unboundedly)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"refresh": "1m",
|
|
||||||
"schemaVersion": 16,
|
|
||||||
"style": "dark",
|
|
||||||
"tags": [],
|
|
||||||
"templating": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"allValue": null,
|
|
||||||
"current": {
|
|
||||||
"text": "zrepl",
|
|
||||||
"value": "zrepl"
|
|
||||||
},
|
|
||||||
"datasource": "prometheus",
|
|
||||||
"hide": 0,
|
|
||||||
"includeAll": false,
|
|
||||||
"label": "Prometheus Job Name",
|
|
||||||
"multi": false,
|
|
||||||
"name": "prom_job_name",
|
|
||||||
"options": [],
|
|
||||||
"query": "label_values(up, job)",
|
|
||||||
"refresh": 1,
|
|
||||||
"regex": "",
|
|
||||||
"sort": 1,
|
|
||||||
"tagValuesQuery": "",
|
|
||||||
"tags": [],
|
|
||||||
"tagsQuery": "",
|
|
||||||
"type": "query",
|
|
||||||
"useTags": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-2d",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {
|
|
||||||
"refresh_intervals": [
|
|
||||||
"5s",
|
|
||||||
"10s",
|
|
||||||
"30s",
|
|
||||||
"1m",
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"30m",
|
|
||||||
"1h",
|
|
||||||
"2h",
|
|
||||||
"1d"
|
|
||||||
],
|
|
||||||
"time_options": [
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timezone": "",
|
|
||||||
"title": "zrepl 0.1",
|
|
||||||
"uid": "xTljn4qmk",
|
|
||||||
"version": 6
|
|
||||||
}
|
|
||||||
+1241
File diff suppressed because it is too large
Load Diff
+115
-8
@@ -6,6 +6,7 @@
|
|||||||
.. |docs| replace:: [DOCS]
|
.. |docs| replace:: [DOCS]
|
||||||
.. |feature| replace:: [FEATURE]
|
.. |feature| replace:: [FEATURE]
|
||||||
.. |mig| replace:: **[MIGRATION]**
|
.. |mig| replace:: **[MIGRATION]**
|
||||||
|
.. |maint| replace:: [MAINT]
|
||||||
|
|
||||||
.. _changelog:
|
.. _changelog:
|
||||||
|
|
||||||
@@ -26,6 +27,120 @@ We use the following annotations for classifying changes:
|
|||||||
* |feature| Change that introduces new functionality.
|
* |feature| Change that introduces new functionality.
|
||||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||||
* |docs| Change to the documentation.
|
* |docs| Change to the documentation.
|
||||||
|
* |maint| Maintenance changes.
|
||||||
|
|
||||||
|
0.4.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
||||||
|
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
||||||
|
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
||||||
|
* |feature| New ``zrepl status`` UI:
|
||||||
|
|
||||||
|
* Interactive job selection.
|
||||||
|
* Interactively ``zrepl signal`` jobs.
|
||||||
|
* Filter filesystems in the job view by name.
|
||||||
|
* An approximation of the old UI is still included as `--mode legacy` but will be removed in a future release of zrepl.
|
||||||
|
|
||||||
|
* |bugfix| Actually use concurrency when listing zrepl abstractions & doing size estimation.
|
||||||
|
These operations were accidentally made sequential in zrepl 0.3.
|
||||||
|
* |bugfix| Job hang-up during second replication attempt.
|
||||||
|
* |bugfix| Data races conditions in the dataconn rpc stack.
|
||||||
|
* |maint| Update to protobuf v1.25 and grpc 1.35.
|
||||||
|
|
||||||
|
For users who skipped the 0.3.1 update: please make sure your pruning grid config is correct.
|
||||||
|
The following bugfix in 0.3.1 :issue:`caused problems for some users <400>`:
|
||||||
|
|
||||||
|
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||||
|
| You can support maintenance and feature development through one of the following services:
|
||||||
|
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
| Note that PayPal processing fees are relatively high for small donations.
|
||||||
|
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
|
0.3.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
Mostly a bugfix release for :ref:`zrepl 0.3 <release-0.3>`.
|
||||||
|
|
||||||
|
* |feature| pruning: add optional ``regex`` field to ``last_n`` rule
|
||||||
|
* |docs| pruning: ``grid`` : improve documentation and add an example
|
||||||
|
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||||
|
This brings the implementation in line with the docs.
|
||||||
|
* |bugfix| ``easyrsa`` script in docs
|
||||||
|
* |bugfix| platformtest: fix skipping encryption-only tests on systems that don't support encryption
|
||||||
|
* |bugfix| replication: report AttemptDone if no filesystems are replicated
|
||||||
|
* |feature| status + replication: warning if replication succeeeded without any filesystem being replicated
|
||||||
|
* |docs| update multi-job & multi-host setup section
|
||||||
|
* RPM Packaging
|
||||||
|
* CI infrastructure rework
|
||||||
|
* Continuous deployment of that new `stable` branch to zrepl.github.io.
|
||||||
|
|
||||||
|
.. _release-0.3:
|
||||||
|
|
||||||
|
0.3
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a big one! Headlining features:
|
||||||
|
|
||||||
|
* **Resumable Send & Recv Support**
|
||||||
|
No knobs required, automatically used where supported.
|
||||||
|
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
|
||||||
|
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||||
|
* **Replication Guarantees**
|
||||||
|
Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
|
||||||
|
By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
We highly recommend studying the updated :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Go 1.15 changed the default TLS validation policy to **require Subject Alternative Names (SAN) in certificates**.
|
||||||
|
The openssl commands we provided in the quick-start guides up to and including the zrepl 0.3 docs seem not to work properly.
|
||||||
|
If you encounter certificate validation errors regarding SAN and wish to continue to use your old certificates, start the zrepl daemon with env var ``GODEBUG=x509ignoreCN=0``.
|
||||||
|
Alternatively, generate new certificates with SANs (see :ref:`both options int the TLS transport docs <transport-tcp+tlsclientauth-certgen>` ).
|
||||||
|
|
||||||
|
Quick-start guides:
|
||||||
|
|
||||||
|
* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl <quickstart-backup-to-external-disk>`.
|
||||||
|
Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk.
|
||||||
|
|
||||||
|
Additional changelog:
|
||||||
|
|
||||||
|
* |break| Go 1.15 TLS changes mentioned above.
|
||||||
|
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
|
||||||
|
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details <zrepl-zfs-abstractions>`).
|
||||||
|
Therefore you might need to adjust your job names.
|
||||||
|
**Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
|
||||||
|
* |break| |mig| replication cursor representation changed
|
||||||
|
|
||||||
|
* zrepl now manages the :ref:`replication cursor bookmark <zrepl-zfs-abstractions>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||||
|
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
||||||
|
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||||
|
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||||
|
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
|
||||||
|
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||||
|
|
||||||
|
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||||
|
* |feature| :issue:`341` Prometheus metric for failing replications + corresponding Grafana panel
|
||||||
|
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
|
||||||
|
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||||
|
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
|
||||||
|
* |feature| logging: trace IDs for better log entry correlation with concurrent replication jobs
|
||||||
|
* |feature| experimental environment variable for parallel replication (see :issue:`306` )
|
||||||
|
* |bugfix| missing logger context vars in control connection handlers
|
||||||
|
* |bugfix| improved error messages on ``zfs send`` errors
|
||||||
|
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||||
|
* |bugfix| transport/ssh: do not leak zombie ssh process on connection failures
|
||||||
|
that will not be snapshotted until the sync-up phase is over
|
||||||
|
* |docs| Installation: :ref:`FreeBSD jail with iocage <installation-freebsd-jail-with-iocage>`
|
||||||
|
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
||||||
|
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||||
|
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
||||||
|
|
||||||
0.2.1
|
0.2.1
|
||||||
-----
|
-----
|
||||||
@@ -63,14 +178,6 @@ We use the following annotations for classifying changes:
|
|||||||
The pool name ``zreplplatformtest`` is reserved for this use case.
|
The pool name ``zreplplatformtest`` is reserved for this use case.
|
||||||
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
|
||||||
| You can support maintenance and feature development through one of the following services:
|
|
||||||
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
|
||||||
| Note that PayPal processing fees are relatively high for small donations.
|
|
||||||
| For SEPA wire transfer and **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
|
|
||||||
|
|
||||||
|
|
||||||
0.1.1
|
0.1.1
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ Configuration
|
|||||||
configuration/jobs
|
configuration/jobs
|
||||||
configuration/transports
|
configuration/transports
|
||||||
configuration/filter_syntax
|
configuration/filter_syntax
|
||||||
|
configuration/sendrecvoptions
|
||||||
|
configuration/replication
|
||||||
configuration/snapshotting
|
configuration/snapshotting
|
||||||
configuration/prune
|
configuration/prune
|
||||||
configuration/logging
|
configuration/logging
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Filter Syntax
|
Filter Syntax
|
||||||
=============
|
=============
|
||||||
|
|
||||||
For :ref:`source jobs <job-source>` and :ref:`push jobs <job-push>`, a filesystem filter must be defined (field ``filesystems``).
|
For :ref:`source<job-source>`, :ref:`push<job-push>` and :ref:`snap<job-snap>` jobs, a filesystem filter must be defined (field ``filesystems``).
|
||||||
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
||||||
|
|
||||||
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ Job Type ``push``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``push``
|
- = ``push``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``connect``
|
* - ``connect``
|
||||||
- |connect-transport|
|
- |connect-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
@@ -45,7 +47,7 @@ Job Type ``sink``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``sink``
|
- = ``sink``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``serve``
|
* - ``serve``
|
||||||
- |serve-transport|
|
- |serve-transport|
|
||||||
* - ``root_fs``
|
* - ``root_fs``
|
||||||
@@ -68,7 +70,7 @@ Job Type ``pull``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``pull``
|
- = ``pull``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``connect``
|
* - ``connect``
|
||||||
- |connect-transport|
|
- |connect-transport|
|
||||||
* - ``root_fs``
|
* - ``root_fs``
|
||||||
@@ -96,11 +98,13 @@ Job Type ``source``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``source``
|
- = ``source``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``serve``
|
* - ``serve``
|
||||||
- |serve-transport|
|
- |serve-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
|
|
||||||
@@ -133,7 +137,7 @@ Job type that only takes snapshots and performs pruning on the local machine.
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``snap``
|
- = ``snap``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted
|
- |filter-spec| for filesystems to be snapshotted
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ The latter is particularly useful in combination with log aggregation services.
|
|||||||
.. WARNING::
|
.. WARNING::
|
||||||
|
|
||||||
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
||||||
Note that TCP buffering in the kernel must first run full becfore messages are dropped.
|
Note that TCP buffering in the kernel must first run full before messages are dropped.
|
||||||
|
|
||||||
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
|
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
|
||||||
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ Prometheus & Grafana
|
|||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9811`` or ``127.0.0.1:9811`` (port 9811 was reserved to zrepl `on the official list <https://github.com/prometheus/prometheus/wiki/Default-port-allocations/_compare/43e495dd251ee328ac0d08b58084665b5c0f7a7e...459195059b55b414193ebeb80c5ba463d2606951>`_).
|
||||||
The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
|
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||||
|
|
||||||
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
||||||
see :repomasterlink:`dist/grafana`.
|
see :repomasterlink:`dist/grafana`.
|
||||||
@@ -29,7 +30,8 @@ The dashboard also contains some advice on which metrics are important to monito
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9091'
|
listen: ':9811'
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+155
-46
@@ -12,7 +12,7 @@ The following paths are considered:
|
|||||||
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
||||||
The command will output nothing and exit with zero status code if the configuration is valid.
|
The command will output nothing and exit with zero status code if the configuration is valid.
|
||||||
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
||||||
Full example configs such as in the :ref:`tutorial` or the :sampleconf:`/` directory might also be helpful.
|
Full example configs such as in the :ref:`quick-start guides <quickstart-toc>` or the :sampleconf:`/` directory might also be helpful.
|
||||||
However, copy-pasting examples is no substitute for reading documentation!
|
However, copy-pasting examples is no substitute for reading documentation!
|
||||||
|
|
||||||
Config File Structure
|
Config File Structure
|
||||||
@@ -26,9 +26,9 @@ Config File Structure
|
|||||||
type: push
|
type: push
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
zrepl is confgured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
||||||
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
||||||
The ``jobs`` section is a list of jobs which we are goind to explain now.
|
The ``jobs`` section is a list of jobs which we are going to explain now.
|
||||||
|
|
||||||
.. _job-overview:
|
.. _job-overview:
|
||||||
|
|
||||||
@@ -39,9 +39,13 @@ A *job* is the unit of activity tracked by the zrepl daemon.
|
|||||||
The ``type`` of a job determines its role in a replication setup and in snapshot management.
|
The ``type`` of a job determines its role in a replication setup and in snapshot management.
|
||||||
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
|
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
|
||||||
|
|
||||||
|
|
||||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
||||||
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
||||||
The passive side responds to requests from the active side after checking its persmissions.
|
The passive side responds to requests from the active side after checking its permissions.
|
||||||
|
|
||||||
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
||||||
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
||||||
@@ -55,8 +59,8 @@ Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job i
|
|||||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
||||||
| | | (snap) | * Remote server to NAS behind NAT |
|
| | | (snap) | * Remote server to NAS behind NAT |
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
| Local replication | | ``push`` + ``sink`` in one config | * Backup to :ref:`locally attached disk <quickstart-backup-to-external-disk>` |
|
||||||
| | | with :ref:`local transport <transport-local>` | |
|
| | | with :ref:`local transport <transport-local>` | * Backup FreeBSD boot pool |
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||||
| | (snap) | | | required |
|
| | (snap) | | | required |
|
||||||
@@ -76,20 +80,25 @@ The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes
|
|||||||
.. TIP::
|
.. TIP::
|
||||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
||||||
|
|
||||||
|
.. _overview-passive-side--client-identity:
|
||||||
|
|
||||||
How the Passive Side Works
|
How the Passive Side Works
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
||||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
using the transport listener type specified in the ``serve`` field of the job configuration.
|
||||||
Each transport listener provides a client's identity to the passive side job.
|
When a client connects, the transport listener performS listener-specific access control (cert validation, IP ACLs, etc)
|
||||||
It uses the client identity for access control:
|
and determines the *client identity*.
|
||||||
|
The passive side job then uses this client identity as follows:
|
||||||
|
|
||||||
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||||
* The ``source`` job has a whitelist of client identities that are allowed pull access.
|
* The ``source`` might, in the future, embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>` in order to support multi-host replication.
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
||||||
|
|
||||||
|
.. _overview-how-replication-works:
|
||||||
|
|
||||||
How Replication Works
|
How Replication Works
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
@@ -97,7 +106,7 @@ One of the major design goals of the replication module is to avoid any duplicat
|
|||||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
||||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||||
|
|
||||||
The following steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
The following high-level steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||||
|
|
||||||
* Plan the replication:
|
* Plan the replication:
|
||||||
|
|
||||||
@@ -105,11 +114,10 @@ The following steps take place during replication and can be monitored using the
|
|||||||
* Build the **replication plan**
|
* Build the **replication plan**
|
||||||
|
|
||||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||||
* Build a list of replication steps
|
* Build a list of **replication steps**
|
||||||
|
|
||||||
* If possible, use incremental sends (``zfs send -i``)
|
* If possible, use incremental and resumable sends
|
||||||
* Otherwise, use full send of most recent snapshot on sender
|
* Otherwise, use full send of most recent snapshot on sender
|
||||||
* Give up on filesystems that cannot be replicated without data loss
|
|
||||||
|
|
||||||
* Retry on errors that are likely temporary (i.e. network failures).
|
* Retry on errors that are likely temporary (i.e. network failures).
|
||||||
* Give up on filesystems where a permanent error was received over RPC.
|
* Give up on filesystems where a permanent error was received over RPC.
|
||||||
@@ -119,32 +127,119 @@ The following steps take place during replication and can be monitored using the
|
|||||||
* Perform replication steps in the following order:
|
* Perform replication steps in the following order:
|
||||||
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
||||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||||
* After a successful replication step, update the replication cursor bookmark (see below).
|
* Acquire send-side *step-holds* on the step's `from` and `to` snapshots.
|
||||||
|
* Perform the replication step.
|
||||||
|
* Move the **replication cursor** bookmark on the sending side (see below).
|
||||||
|
* Move the **last-received-hold** on the receiving side (see below).
|
||||||
|
* Release the send-side step-holds.
|
||||||
|
|
||||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||||
|
|
||||||
.. _replication-cursor-bookmark:
|
ZFS Background Knowledge
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
|
||||||
|
Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
||||||
|
|
||||||
The **replication cursor bookmark** ``#zrepl_replication_cursor`` is kept per filesystem on the sending side of a replication setup:
|
**ZFS Send Modes & Bookmarks**
|
||||||
It is a bookmark of the most recent successfully replicated snapshot to the receiving side.
|
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
|
||||||
It is is used by the :ref:`not_replicated <prune-keep-not-replicated>` keep rule to identify all snapshots that have not yet been replicated to the receiving side.
|
Full sends are used to create a new filesystem on the receiver with the send-side state of ``fs@to``.
|
||||||
Regardless of whether that keep rule is used, the bookmark ensures that replication can always continue incrementally.
|
Incremental sends only transfer the delta between ``@from`` and ``@to``.
|
||||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
Incremental sends require that ``@from`` be present on the receiving side when receiving the incremental stream.
|
||||||
|
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
|
||||||
|
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
|
||||||
|
|
||||||
|
.. _zfs-background-knowledge-plain-vs-raw-sends:
|
||||||
|
|
||||||
|
**Plain and raw sends**
|
||||||
|
By default, ``zfs send`` sends the most generic, backwards-compatible data stream format (so-called 'plain send').
|
||||||
|
If the sent uses newer features, e.g. compression or encryption, ``zfs send`` has to un-do these operations on the fly to produce the plain send stream.
|
||||||
|
If the receiver uses newer features (e.g. compression or encryption inherited from the parent FS), it applies the necessary transformations again on the fly during ``zfs recv``.
|
||||||
|
|
||||||
|
Flags such as ``-e``, ``-c`` and ``-L`` tell ZFS to produce a send stream that is closer to how the data is stored on disk.
|
||||||
|
Sending with those flags removes computational overhead from sender and receiver.
|
||||||
|
However, the receiver will not apply certain transformations, e.g., it will not compress with the receive-side ``compression`` algorithm.
|
||||||
|
|
||||||
|
The ``-w`` (``--raw``) flag produces a send stream that is as *raw* as possible.
|
||||||
|
For unencrypted datasets, its current effect is the same as ``-Lce``.
|
||||||
|
|
||||||
|
Encrypted datasets can only be sent plain (unencrypted) or raw (encrypted) using the ``-w`` flag.
|
||||||
|
|
||||||
|
**Resumable Send & Recv**
|
||||||
|
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
|
||||||
|
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
|
||||||
|
A full send can only be resumed if ``@to`` still exists.
|
||||||
|
An incremental send can only be resumed if ``@to`` still exists *and* either ``@from`` still exists *or* a bookmark ``#fbm`` of ``@from`` still exists.
|
||||||
|
|
||||||
|
**ZFS Holds**
|
||||||
|
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
|
||||||
|
Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
||||||
|
|
||||||
|
|
||||||
|
.. _zrepl-zfs-abstractions:
|
||||||
|
|
||||||
|
ZFS Abstractions Managed By zrepl
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality.
|
||||||
|
|
||||||
.. _replication-placeholder-property:
|
.. _replication-placeholder-property:
|
||||||
|
|
||||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the ZFS property ``zrepl:placeholder=on``.
|
||||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
||||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||||
|
|
||||||
|
.. _replication-cursor-and-last-received-hold:
|
||||||
|
|
||||||
|
The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
||||||
|
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||||
|
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||||
|
Both are moved atomically after the receiving side has confirmed that a replication step is complete.
|
||||||
|
|
||||||
|
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||||
|
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
|
||||||
|
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||||
|
|
||||||
|
.. _tentative-replication-cursor-bookmarks:
|
||||||
|
|
||||||
|
**Tentative replication cursor bookmarks** are short-lived bookmarks that protect the atomic moving-forward of the replication cursor and last-received-hold (see :issue:`this issue <340>`).
|
||||||
|
They are only necessary if step holds are not used as per the :ref:`replication.protection <replication-option-protection>` setting.
|
||||||
|
The tentative replication cursor has the format ``#zrepl_CUSORTENTATIVE_G_<GUID>_J_<JOBNAME>``.
|
||||||
|
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||||
|
|
||||||
|
.. _step-holds:
|
||||||
|
|
||||||
|
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
|
||||||
|
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
|
||||||
|
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
|
||||||
|
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
|
||||||
|
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
|
||||||
|
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
|
||||||
|
A job only ever has one active send per filesystem.
|
||||||
|
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
|
||||||
|
|
||||||
|
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
|
||||||
|
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl.
|
||||||
|
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
|
||||||
|
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
|
||||||
|
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
|
||||||
|
Subscribe to zrepl :issue:`326` for details.
|
||||||
|
|
||||||
|
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||||
|
|
||||||
|
Limitations
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
.. ATTENTION::
|
.. ATTENTION::
|
||||||
|
|
||||||
Currently, zrepl does not replicate filesystem properties.
|
Currently, zrepl does not replicate filesystem properties.
|
||||||
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||||
This is temporary and being worked on :issue:`24`.
|
This is temporary and being worked on :issue:`24`.
|
||||||
|
|
||||||
|
|
||||||
@@ -153,39 +248,53 @@ The ``zrepl test placeholder`` command can be used to check whether a filesystem
|
|||||||
Multiple Jobs & More than 2 Machines
|
Multiple Jobs & More than 2 Machines
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
The quick-start guides focus on simple setups with a single sender and a single receiver.
|
||||||
|
This section documents considerations for more complex setups.
|
||||||
|
|
||||||
.. ATTENTION::
|
.. ATTENTION::
|
||||||
|
|
||||||
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
Before you continue, make sure you have a working understanding of :ref:`how zrepl works <overview-how-replication-works>`
|
||||||
|
and :ref:`what zrepl does to ensure <zrepl-zfs-abstractions>` that replication between sender and receiver is always
|
||||||
|
possible without conflicts.
|
||||||
|
This will help you understand why certain kinds of multi-machine setups do not (yet) work.
|
||||||
|
|
||||||
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
.. NOTE::
|
||||||
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
|
||||||
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
|
||||||
|
|
||||||
Explanations & exceptions to above rules are detailed below.
|
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||||
|
|
||||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
Multiple Jobs on one Machine
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
|
||||||
|
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
||||||
|
|
||||||
No Overlapping
|
On your setup, ensure that
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jobs run independently of each other.
|
* all ``filesystems`` filter specifications are disjoint
|
||||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
* no ``root_fs`` is a prefix or equal to another ``root_fs``
|
||||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
* no ``filesystems`` filter matches any ``root_fs``
|
||||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
|
||||||
|
|
||||||
N push jobs to 1 sink
|
**Exceptions to the rule**:
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
* A ``snap`` and ``push`` job on the same machine can match the same ``filesystems``.
|
||||||
It is thus safe to push to one sink job with different client identities.
|
To avoid interference, only one of the jobs should be pruning snapshots on the sender, the other one should keep all snapshots.
|
||||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
Since the jobs won't coordinate, errors in the log are to be expected, but :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` ensure that ``push`` and ``sink`` can always replicate incrementally.
|
||||||
|
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
||||||
|
|
||||||
N pull jobs from 1 source
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
More Than 2 Machines
|
||||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
|
||||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
|
||||||
|
**Working setups**:
|
||||||
|
|
||||||
|
* N ``push`` identities, 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
|
||||||
|
|
||||||
|
* ``sink`` constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
|
||||||
|
Therefore, the different clients cannot interfere.
|
||||||
|
|
||||||
|
|
||||||
|
**Setups that do not work**:
|
||||||
|
|
||||||
|
* N ``pull`` identities, 1 ``source`` job. Tracking :issue:`380`.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Pruning Policies
|
|||||||
================
|
================
|
||||||
|
|
||||||
In zrepl, *pruning* means *destroying snapshots*.
|
In zrepl, *pruning* means *destroying snapshots*.
|
||||||
Pruning must happen on both sides of a replication or the systems would inevitable run out of disk space at some point.
|
Pruning must happen on both sides of a replication or the systems would inevitably run out of disk space at some point.
|
||||||
|
|
||||||
Typically, the requirements to temporal resolution and maximum retention time differ per side.
|
Typically, the requirements to temporal resolution and maximum retention time differ per side.
|
||||||
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
|
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
|
||||||
@@ -46,6 +46,8 @@ Example Configuration:
|
|||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
# manually created snapshots will be kept forever on receiver
|
# manually created snapshots will be kept forever on receiver
|
||||||
|
- type: regex
|
||||||
|
regex: "^manual_.*"
|
||||||
|
|
||||||
.. DANGER::
|
.. DANGER::
|
||||||
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
||||||
@@ -65,8 +67,9 @@ Policy ``not_replicated``
|
|||||||
...
|
...
|
||||||
|
|
||||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||||
It only makes sense to specify this rule on a sender (source or push job).
|
It only makes sense to specify this rule for the ``keep_sender``.
|
||||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-bookmark>` on the sending side.
|
The reason is that, by definition, all snapshots on the receiver have already been replicated to there from the sender.
|
||||||
|
To determine whether a sender-side snapshot has already been replicated, zrepl uses the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` which corresponds to the most recent successfully replicated snapshot.
|
||||||
|
|
||||||
.. _prune-keep-retention-grid:
|
.. _prune-keep-retention-grid:
|
||||||
|
|
||||||
@@ -82,35 +85,91 @@ Policy ``grid``
|
|||||||
- type: grid
|
- type: grid
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
│ │
|
│ │ │
|
||||||
└─ one hour interval
|
└─ 1 repetition of a one-hour interval with keep=all
|
||||||
│
|
│ │
|
||||||
└─ 24 adjacent one-hour intervals
|
└─ 24 repetitions of a one-hour interval with keep=1
|
||||||
|
│
|
||||||
|
└─ 6 repetitions of a 30-day interval with keep=1
|
||||||
...
|
...
|
||||||
|
|
||||||
The retention grid can be thought of as a time-based sieve:
|
The retention grid can be thought of as a time-based sieve that thins out snapshots as they get older.
|
||||||
The ``grid`` field specifies a list of adjacent time intervals:
|
|
||||||
the left edge of the leftmost (first) interval is the ``creation`` date of the youngest snapshot.
|
|
||||||
All intervals to its right describe time intervals further in the past.
|
|
||||||
|
|
||||||
Each interval carries a maximum number of snapshots to keep.
|
|
||||||
It is specified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
|
|
||||||
The default value is **keep=1**.
|
|
||||||
|
|
||||||
|
The ``grid`` field specifies a list of adjacent time intervals.
|
||||||
|
Each interval is a bucket with a maximum capacity of ``keep`` snapshots.
|
||||||
The following procedure happens during pruning:
|
The following procedure happens during pruning:
|
||||||
|
|
||||||
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
||||||
Only snapshots names that match the regex are considered for this rule, all others are not affected.
|
Only snapshots names that match the regex are considered for this rule, all others will be pruned unless another rule keeps them.
|
||||||
#. The filtered list of snapshots is sorted by ``creation``
|
#. The snapshots that match ``regex`` are placed onto a time axis according to their ``creation`` date.
|
||||||
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot
|
The youngest snapshot is on the left, the oldest on the right.
|
||||||
#. A list of buckets is created, one for each interval
|
#. The first buckets are placed "under" that axis so that the ``grid`` spec's first bucket's left edge aligns with youngest snapshot.
|
||||||
#. The list of snapshots is split up into the buckets.
|
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
||||||
#. For each bucket
|
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
||||||
|
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
||||||
|
#. Snapshots older than the rightmost bucket **not kept** by this gridspec.
|
||||||
|
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
||||||
|
|
||||||
#. the contained snapshot list is sorted by creation.
|
The syntax to describe the bucket list is as follows:
|
||||||
#. snapshots from the list, oldest first, are destroyed until the specified ``keep`` count is reached.
|
|
||||||
#. all remaining snapshots on the list are kept.
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Repeat x Duration (keep=all)
|
||||||
|
|
||||||
|
* The **duration** specifies the length of the interval.
|
||||||
|
* The **keep** count specifies the number of snapshots that fit into the bucket.
|
||||||
|
It can be either a positive integer or ``all`` (all snapshots are kept).
|
||||||
|
* The **repeat** count repeats the bucket definition for the specified number of times.
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Assume the following grid spec:
|
||||||
|
|
||||||
|
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||||
|
|
||||||
|
This grid spec produces the following constellation of buckets:
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Now assume that we have a set of snapshots @a, @b, ..., @D.
|
||||||
|
Snapshot @a is the most recent snapshot.
|
||||||
|
Snapshot @D is the oldest snapshot, it is almost 9 hours older than snapshot @a.
|
||||||
|
We place the snapshots on the same timeline as the buckets:
|
||||||
|
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|
| | | | |
|
||||||
|
| a b c| d e f g h i j k l m n o p |q r s t u v w x y z |A B C D
|
||||||
|
|
||||||
|
The result is the following mapping of snapshots to buckets:
|
||||||
|
|
||||||
|
Bucket1: a, b, c
|
||||||
|
Bucket2: d,e,f,g,h,i,j
|
||||||
|
Bucket3: k,l,m,n,o,p
|
||||||
|
Bucket4: q,r,s,t,u,v,w,x,y,z
|
||||||
|
No bucket: A,B,C,D
|
||||||
|
|
||||||
|
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
||||||
|
Newer snapshots are destroyed first.
|
||||||
|
Snapshots that do not fall into a bucket are always destroyed.
|
||||||
|
|
||||||
|
Result after pruning:
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| | | | |
|
||||||
|
| a b c| j p | z |
|
||||||
|
|
||||||
.. _prune-keep-last-n:
|
.. _prune-keep-last-n:
|
||||||
|
|
||||||
@@ -125,9 +184,11 @@ Policy ``last_n``
|
|||||||
keep_receiver:
|
keep_receiver:
|
||||||
- type: last_n
|
- type: last_n
|
||||||
count: 10
|
count: 10
|
||||||
|
regex: ^zrepl_.*$ # optional
|
||||||
...
|
...
|
||||||
|
|
||||||
``last_n`` keeps the last ``count`` snapshots (last = youngest = most recent creation date).
|
``last_n`` filters the snapshot list by ``regex``, then keeps the last ``count`` snapshots in that list (last = youngest = most recent creation date)
|
||||||
|
All snapshots that don't match ``regex`` or exceed ``count`` in the filtered list are destroyed unless matched by other rules.
|
||||||
|
|
||||||
.. _prune-keep-regex:
|
.. _prune-keep-regex:
|
||||||
|
|
||||||
@@ -154,7 +215,7 @@ Policy ``regex``
|
|||||||
negate: true
|
negate: true
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
|
|
||||||
``regex`` keeps all snapshots whose names are matched by the regular expressionin ``regex``.
|
``regex`` keeps all snapshots whose names are matched by the regular expression in ``regex``.
|
||||||
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
|
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
|
||||||
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
|
||||||
|
Replication Options
|
||||||
|
===================
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: ...
|
||||||
|
replication:
|
||||||
|
protection:
|
||||||
|
initial: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||||
|
incremental: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||||
|
concurrency:
|
||||||
|
size_estimates: 4
|
||||||
|
steps: 1
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
.. _replication-option-protection:
|
||||||
|
|
||||||
|
``protection`` option
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The ``protection`` variable controls the degree to which a replicated filesystem is protected from getting out of sync through a zrepl pruner or external tools that destroy snapshots.
|
||||||
|
zrepl can guarantee :ref:`resumability <step-holds>` or just :ref:`incremental replication <replication-cursor-and-last-received-hold>`.
|
||||||
|
|
||||||
|
``guarantee_resumability`` is the **default** value and guarantees that a replication step is always resumable and that incremental replication will always be possible.
|
||||||
|
The implementation uses replication cursors, last-received-hold and step holds.
|
||||||
|
|
||||||
|
``guarantee_incremental`` only guarantees that incremental replication will always be possible.
|
||||||
|
If a step ``from -> to`` is interrupted and its `to` snapshot is destroyed, zrepl will remove the half-received ``to``'s resume state and start a new step ``from -> to2``.
|
||||||
|
The implementation uses replication cursors, tentative replication cursors and last-received-hold.
|
||||||
|
|
||||||
|
``guarantee_nothing`` does not make any guarantees with regards to keeping sending and receiving side in sync.
|
||||||
|
No bookmarks or holds are created to protect sender and receiver from diverging.
|
||||||
|
|
||||||
|
**Tradeoffs**
|
||||||
|
|
||||||
|
Using ``guarantee_incremental`` instead of ``guarantee_resumability`` obviously removes the resumability guarantee.
|
||||||
|
This means that replication progress is no longer monotonic which might lead to a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
|
||||||
|
However, the advantage and :issue:`reason for existence <288>` of the ``incremental`` mode is that it allows the pruner to delete snapshots of interrupted replication steps
|
||||||
|
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
When changing this flag, obsoleted zrepl-managed bookmarks and holds will be destroyed on the next replication step that is attempted for each filesystem.
|
||||||
|
|
||||||
|
|
||||||
|
.. _replication-option-concurrency:
|
||||||
|
|
||||||
|
``concurrency`` option
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The ``concurrency`` options control the maximum amount of concurrency during replication.
|
||||||
|
The default values allow some concurrency during size estimation but no parallelism for the actual replication.
|
||||||
|
|
||||||
|
* ``concurrency.steps`` (default = 1) controls the maximum number of concurrently executed :ref:`replication steps <overview-how-replication-works>`.
|
||||||
|
The planning step for each file system is counted as a single step.
|
||||||
|
* ``concurrency.size_estimates`` (default = 4) controls the maximum number of concurrent step size estimations done by the job.
|
||||||
|
|
||||||
|
Note that initial replication cannot start replicating child filesystems before the parent filesystem's initial replication step has completed.
|
||||||
|
|
||||||
|
Some notes on tuning these values:
|
||||||
|
|
||||||
|
* Disk: Size estimation is less I/O intensive than step execution because it does not need to access the data blocks.
|
||||||
|
* CPU: Size estimation is usually a dense CPU burst whereas step execution CPU utilization is stretched out over time because of disk IO.
|
||||||
|
Faster disks, sending a compressed dataset in :ref:`plain mode <zfs-background-knowledge-plain-vs-raw-sends>` and the zrepl transport mode all contribute to higher CPU requirements.
|
||||||
|
* Network bandwidth: Size estimation does not consume meaningful amounts of bandwidth, step execution does.
|
||||||
|
* :ref:`zrepl ZFS abstractions <zrepl-zfs-abstractions>`: for each replication step zrepl needs to update its ZFS abstractions through the ``zfs`` command which often waits multiple seconds for the zpool to sync.
|
||||||
|
Thus, if the actual send & recv time of a step is small compared to the time spent on zrepl ZFS abstractions then increasing step execution concurrency will result in a lower overall turnaround time.
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
|
||||||
|
Send & Recv Options
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. _job-send-options:
|
||||||
|
|
||||||
|
Send Options
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: ...
|
||||||
|
send:
|
||||||
|
# flags from the table below go here
|
||||||
|
...
|
||||||
|
|
||||||
|
The following table specifies the list of (boolean) options.
|
||||||
|
Flags with an entry in the ``zfs send`` column map directly to the zfs send CLI flags.
|
||||||
|
zrepl does not perform feature checks for these flags.
|
||||||
|
If you enable a flag that is not supported by the installed version of ZFS, the zfs error will show up at runtime in the logs and zrepl status.
|
||||||
|
See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-send.8.html>`_ (``man zfs-send``) for their semantics.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 10 70
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - ``send.``
|
||||||
|
- ``zfs send``
|
||||||
|
- Comment
|
||||||
|
* - ``encrypted``
|
||||||
|
-
|
||||||
|
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
||||||
|
* - ``bandwidth_limit``
|
||||||
|
-
|
||||||
|
- Specific to zrepl, :ref:`see below <job-send-recv-options-bandwidth-limit>`.
|
||||||
|
* - ``raw``
|
||||||
|
- ``-w``
|
||||||
|
- Use ``encrypted`` to only allow encrypted sends.
|
||||||
|
* - ``send_properties``
|
||||||
|
- ``-p``
|
||||||
|
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
* - ``backup_properties``
|
||||||
|
- ``-b``
|
||||||
|
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
* - ``large_blocks``
|
||||||
|
- ``-L``
|
||||||
|
- **Potential data loss on OpenZFS < 2.0**, see :ref:`warning below <job-send-options-large-blocks>`.
|
||||||
|
* - ``compressed``
|
||||||
|
- ``-c``
|
||||||
|
-
|
||||||
|
* - ``embbeded_data``
|
||||||
|
- ``-e``
|
||||||
|
-
|
||||||
|
* - ``saved``
|
||||||
|
- ``-S``
|
||||||
|
-
|
||||||
|
|
||||||
|
.. _job-send-options-encrypted:
|
||||||
|
|
||||||
|
``encrypted``
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The ``encrypted`` option controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
||||||
|
More specifically, if ``encrypted=true``, zrepl
|
||||||
|
|
||||||
|
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
|
||||||
|
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
||||||
|
* expects the receiving side to support OpenZFS native encryption (recv will fail otherwise)
|
||||||
|
|
||||||
|
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
|
||||||
|
|
||||||
|
If ``encrypted=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Use ``encrypted`` instead of ``raw`` to make your intent clear that zrepl must only replicate filesystems that are actually encrypted by OpenZFS native encryption.
|
||||||
|
It is meant as a safeguard to prevent unintended sends of unencrypted filesystems in raw mode.
|
||||||
|
|
||||||
|
.. _job-send-options-properties:
|
||||||
|
|
||||||
|
``properties``
|
||||||
|
--------------
|
||||||
|
Sends the dataset properties along with snapshots.
|
||||||
|
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
|
||||||
|
.. _job-send-options-backup-properties:
|
||||||
|
|
||||||
|
``backup_properties``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
When properties are modified on a filesystem that was received from a send stream with ``send.properties=true``, ZFS archives the original received value internally.
|
||||||
|
This also applies to :ref:`inheriting or overriding properties during zfs receive <job-recv-options--inherit-and-override>`.
|
||||||
|
|
||||||
|
When sending those received filesystems another hop, the ``backup_properties`` flag instructs ZFS to send the original property values rather than the current locally set values.
|
||||||
|
|
||||||
|
This is useful for replicating properties across multiple levels of backup machines.
|
||||||
|
**Example:**
|
||||||
|
Suppose we want to flow snapshots from Machine A to B, then from B to C.
|
||||||
|
A will enable the :ref:`properties send option <job-send-options-properties>`.
|
||||||
|
B will want to override :ref:`critical properties such as mountpoint or canmount <job-note-property-replication>`.
|
||||||
|
But the job that replicates from B to C should be sending the original property values received from A.
|
||||||
|
Thus, B sets the ``backup_properties`` option.
|
||||||
|
|
||||||
|
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
|
||||||
|
.. _job-send-options-large-blocks:
|
||||||
|
|
||||||
|
``large_blocks``
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This flag should not be changed after initial replication.
|
||||||
|
Prior to `OpenZFS commit 7bcb7f08 <https://github.com/openzfs/zfs/pull/10383/files#diff-4c1e47568f46fb63546e984943b09a3e6b051e2242649523f7835bbdfe2a9110R337-R342>`_
|
||||||
|
it was possible to change this setting which resulted in **data loss on the receiver**.
|
||||||
|
The commit in question is included in OpenZFS 2.0 and works around the problem by prohibiting receives of incremental streams with a flipped setting.
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
This bug has **not been fixed in the OpenZFS 0.8 releases** which means that changing this flag after initial replication might cause **data loss** on the receiver.
|
||||||
|
|
||||||
|
.. _job-recv-options:
|
||||||
|
|
||||||
|
Recv Options
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: pull
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
inherit:
|
||||||
|
- "mountpoint"
|
||||||
|
override: {
|
||||||
|
"org.openzfs.systemd:ignore": "on"
|
||||||
|
}
|
||||||
|
bandwidth_limit: ... # see below
|
||||||
|
...
|
||||||
|
|
||||||
|
.. _job-recv-options--inherit-and-override:
|
||||||
|
|
||||||
|
``properties``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
``override`` maps directly to the `zfs recv -o flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
||||||
|
Property name-value pairs specified in this map will apply to all received filesystems, regardless of whether the send stream contains properties or not.
|
||||||
|
|
||||||
|
``inherit`` maps directly to the `zfs recv -x flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
||||||
|
Property names specified in this list will be inherited from the receiving side's parent filesystem (e.g. ``root_fs``).
|
||||||
|
|
||||||
|
With both options, the sending side's property value is still stored on the receiver, but the local override or inherit is the one that takes effect.
|
||||||
|
You can send the original properties from the first receiver to another receiver using :ref:`send.backup_properties<job-send-options-backup-properties>`.
|
||||||
|
|
||||||
|
|
||||||
|
.. _job-note-property-replication:
|
||||||
|
|
||||||
|
A Note on Property Replication
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If a send stream contains properties, as per ``send.properties`` or ``send.backup_properties``,
|
||||||
|
the default ZFS behavior is to use those properties on the receiving side, verbatim.
|
||||||
|
|
||||||
|
In many use cases for zrepl, this can have devastating consequences.
|
||||||
|
For example, when backing up a filesystem that has ``mountpoint=/`` to a storage server,
|
||||||
|
that storage server's root filesystem will be shadowed by the received file system on some platforms.
|
||||||
|
Also, many scripts and tools use ZFS user properties for configuration and do not check the property source (``local`` vs. ``received``).
|
||||||
|
If they are installed on the receiving side as well as the sending side, property replication could have unintended effects.
|
||||||
|
|
||||||
|
**zrepl currently does not provide any automatic safe-guards for property replication:**
|
||||||
|
|
||||||
|
* Make sure to read the entire man page on zfs recv (`man zfs recv <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_) before enabling this feature.
|
||||||
|
* Use ``recv.properties.override`` whenever possible, e.g. for ``mountpoint=none`` or ``canmount=off``.
|
||||||
|
* Use ``recv.properties.inherit`` if that makes more sense to you.
|
||||||
|
|
||||||
|
Below is an **non-exhaustive list of problematic properties**.
|
||||||
|
Please open a pull request if you find a property that is missing from this list.
|
||||||
|
(Both with regards to core ZFS tools and other software in the broader ecosystem.)
|
||||||
|
|
||||||
|
Mount behaviour
|
||||||
|
---------------
|
||||||
|
|
||||||
|
* ``mountpoint``
|
||||||
|
* ``canmount``
|
||||||
|
* ``overlay``
|
||||||
|
|
||||||
|
Note: inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
|
||||||
|
This is an `issue in OpenZFS <https://github.com/openzfs/zfs/issues/11416>`_ .
|
||||||
|
As a workaround, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
|
||||||
|
Please comment at zrepl :issue:`430` if you encounter this issue and/or would like zrepl to automatically work around it.
|
||||||
|
|
||||||
|
|
||||||
|
Systemd
|
||||||
|
-------
|
||||||
|
|
||||||
|
With systemd, you should also consider the properties processed by the `zfs-mount-generator <https://manpages.debian.org/buster-backports/zfsutils-linux/zfs-mount-generator.8.en.html>`_ .
|
||||||
|
|
||||||
|
Most notably:
|
||||||
|
|
||||||
|
* ``org.openzfs.systemd:ignore``
|
||||||
|
* ``org.openzfs.systemd:wanted-by``
|
||||||
|
* ``org.openzfs.systemd:required-by``
|
||||||
|
|
||||||
|
Encryption
|
||||||
|
----------
|
||||||
|
|
||||||
|
If the sender filesystems are encrypted but the sender does :ref:`plain sends <zfs-background-knowledge-plain-vs-raw-sends>`
|
||||||
|
and property replication is enabled, the receiver must :ref:`inherit the following properties<job-recv-options--inherit-and-override>`:
|
||||||
|
|
||||||
|
* ``keylocation``
|
||||||
|
* ``keyformat``
|
||||||
|
* ``encryption``
|
||||||
|
|
||||||
|
Common Options
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. _job-send-recv-options-bandwidth-limit:
|
||||||
|
|
||||||
|
Bandwidth Limit (send & recv)
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
bandwidth_limit:
|
||||||
|
max: 23.5 MiB # -1 is the default and disabled rate limiting
|
||||||
|
bucket_capacity: # token bucket capacity in bytes; defaults to 128KiB
|
||||||
|
|
||||||
|
Both ``send`` and ``recv`` can be limited to a maximum bandwidth through ``bandwidth_limit``.
|
||||||
|
For most users, it should be sufficient to just set ``bandwidth_limit.max``.
|
||||||
|
The ``bandwidth_limit.bucket_capacity`` refers to the `token bucket size <https://github.com/juju/ratelimit>`_.
|
||||||
|
|
||||||
|
The bandwidth limit only applies to the payload data, i.e., the ZFS send stream.
|
||||||
|
It does not account for transport protocol overheads.
|
||||||
|
The scope is the job level, i.e., all :ref:`concurrent <replication-option-concurrency>` sends or incoming receives of a job share the bandwidth limit.
|
||||||
@@ -9,8 +9,15 @@ The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snaps
|
|||||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||||
|
|
||||||
|
When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
|
||||||
|
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||||
|
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||||
|
|
||||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||||
|
|
||||||
|
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||||
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -124,7 +131,7 @@ The following environment variables are set:
|
|||||||
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
||||||
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
||||||
|
|
||||||
An empty template hook can be found in :sampleconf:`hooks/template.sh`.
|
An empty template hook can be found in :sampleconf:`/hooks/template.sh`.
|
||||||
|
|
||||||
.. _job-hook-type-postgres-checkpoint:
|
.. _job-hook-type-postgres-checkpoint:
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user