Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd30afaaf9 | |||
| 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 | |||
| 0261dbfe3d | |||
| 4301f741db | |||
| 7e743c74dc | |||
| ad0b055245 | |||
| 27db3e6f70 | |||
| 4994b7a9ea | |||
| 080f2c0616 | |||
| d469cc04b6 | |||
| e0a25d04ac | |||
| b0f2c79944 | |||
| d2bc40f78d | |||
| 9e54f11960 | |||
| 6eda1f743f | |||
| 6787decef1 | |||
| d56d45a2ab | |||
| fcf16a163a | |||
| dc39c819a3 | |||
| 1048b09487 | |||
| 3806e97404 | |||
| c396f9508a | |||
| b9933f6cb2 | |||
| 0ba4b5eda6 | |||
| 18d2c350de | |||
| f8f9fd11cd | |||
| b422e6f12e | |||
| f8d5082bdd | |||
| ffe677e55a | |||
| 84eefa57bc | |||
| ad77371e38 | |||
| c524acb2df | |||
| 3edfe535c6 | |||
| d3b99e8e39 | |||
| 3c03f21419 | |||
| 5c95c21727 | |||
| a6b578b648 | |||
| 8af824df41 | |||
| 58ab25919e | |||
| 215848f476 | |||
| f9c7766073 | |||
| f976212ec9 | |||
| 8c88e168c1 | |||
| a78c854404 | |||
| 8a5af2f80e | |||
| f7aa26d418 | |||
| b5ff1a9926 | |||
| 729c83ee72 | |||
| 00434f4ac9 | |||
| 2cd9173bfb | |||
| 7ba3ae077f | |||
| a6497b2c6e | |||
| 07956c2299 | |||
| 77d3a1ad4d | |||
| 2e0ff9a582 | |||
| a65d8f1f4c | |||
| 424234c2d1 | |||
| 3bfe0c16d0 | |||
| e5f944c2f8 | |||
| d81a1818d6 | |||
| 921b34235e | |||
| a0cf9cff2a | |||
| 3105609d39 | |||
| 254a292362 | |||
| 95e16f01c1 | |||
| df21b5d1b4 | |||
| c4be60c29f | |||
| ff18181064 | |||
| b167de3096 | |||
| 9dd187e29d | |||
| 413bd0f43b | |||
| 3c3606d516 | |||
| b5dc04e62c | |||
| 234a327a03 | |||
| 90b32c7377 | |||
| 0fc6a63564 | |||
| 5138681c13 | |||
| d6304f4f1d |
+357
-81
@@ -1,102 +1,378 @@
|
|||||||
version: 2.0
|
version: 2.1
|
||||||
workflows:
|
|
||||||
version: 2
|
commands:
|
||||||
build:
|
setup-home-local-bin:
|
||||||
jobs:
|
|
||||||
- build-1.11
|
|
||||||
- build-1.12
|
|
||||||
- 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"'
|
||||||
|
if grep "$line" $BASH_ENV >/dev/null; then
|
||||||
- restore_cache:
|
echo "$line" >> $BASH_ENV
|
||||||
keys:
|
fi
|
||||||
- source
|
|
||||||
- vendor
|
|
||||||
- protobuf
|
|
||||||
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
key: source
|
|
||||||
paths:
|
|
||||||
- ".git"
|
|
||||||
|
|
||||||
# install deps
|
|
||||||
- run: wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
|
||||||
- run: echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
|
||||||
- run: sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
|
||||||
- save_cache:
|
|
||||||
key: protobuf
|
|
||||||
paths:
|
|
||||||
- "/usr/include/google/protobuf"
|
|
||||||
|
|
||||||
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
|
||||||
- run: ./lazy.sh devsetup
|
|
||||||
|
|
||||||
|
invoke-lazy-sh:
|
||||||
|
parameters:
|
||||||
|
subcommand:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
- run:
|
- run:
|
||||||
name: Install minio client, prepare minio
|
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:
|
||||||
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
save-cache-gomod:
|
||||||
|
steps:
|
||||||
|
- save_cache:
|
||||||
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
paths:
|
||||||
|
- "/go/pkg/mod"
|
||||||
|
|
||||||
|
install-godep:
|
||||||
|
steps:
|
||||||
|
- apt-update-and-install-common-deps
|
||||||
|
- 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: |
|
command: |
|
||||||
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
|
if which mc; then exit 0; fi
|
||||||
chmod +x ${GOPATH}/bin/mc
|
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:
|
||||||
|
key: minio-client-v2
|
||||||
|
paths:
|
||||||
|
- "$HOME/.local/bin/mc"
|
||||||
|
|
||||||
|
upload-minio:
|
||||||
|
parameters:
|
||||||
|
src:
|
||||||
|
type: string
|
||||||
|
dst:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
when: always
|
||||||
|
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
|
||||||
|
|
||||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||||
|
|
||||||
- run: make vendordeps
|
# keep in sync with set-github-minio-status
|
||||||
- save_cache:
|
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||||
key: vendor
|
|
||||||
paths:
|
|
||||||
- "./vendor"
|
|
||||||
|
|
||||||
- run: make
|
# Upload artifacts
|
||||||
- run: make vet
|
mkdir -p ./artifacts
|
||||||
- run: make test
|
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
|
||||||
- run: make lint
|
|
||||||
- run: make release
|
|
||||||
|
|
||||||
|
|
||||||
- store_artifacts:
|
|
||||||
path: ./artifacts/release
|
|
||||||
when: always
|
|
||||||
|
|
||||||
|
set-github-minio-status:
|
||||||
|
parameters:
|
||||||
|
context:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
minio-dst:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
- run:
|
- run:
|
||||||
shell: /bin/bash -euo pipefail
|
shell: /bin/bash -eo pipefail
|
||||||
when: always
|
|
||||||
command: |
|
command: |
|
||||||
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
|
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||||
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
|
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
|
||||||
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:
|
trigger-pipeline:
|
||||||
<<: *build-latest
|
parameters:
|
||||||
|
body_no_shell_subst:
|
||||||
|
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>>'
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
do_ci:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
do_release:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
release_docker_baseimage_tag:
|
||||||
|
type: string
|
||||||
|
default: "1.15"
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
ci:
|
||||||
|
when: << pipeline.parameters.do_ci >>
|
||||||
|
jobs:
|
||||||
|
- quickcheck-docs
|
||||||
|
- quickcheck-go: &quickcheck-go-smoketest
|
||||||
|
name: quickcheck-go-amd64-linux-1.15
|
||||||
|
goversion: &latest-go-release "1.15"
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- test-go-on-latest-go-release:
|
||||||
|
goversion: *latest-go-release
|
||||||
|
- quickcheck-go:
|
||||||
|
requires:
|
||||||
|
- quickcheck-go-amd64-linux-1.15 #quickcheck-go-smoketest.name
|
||||||
|
matrix: &quickcheck-go-matrix
|
||||||
|
alias: quickcheck-go-matrix
|
||||||
|
parameters:
|
||||||
|
goversion: [*latest-go-release, "1.11"]
|
||||||
|
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.11
|
||||||
|
- goversion: "1.11"
|
||||||
|
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.11
|
- image: cimg/base:2020.08
|
||||||
build-1.12:
|
steps:
|
||||||
<<: *build-latest
|
- checkout
|
||||||
|
- install-docdep
|
||||||
|
- run: make docs
|
||||||
|
|
||||||
|
- store_artifacts:
|
||||||
|
path: artifacts
|
||||||
|
- 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:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- 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 "me@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"'" }}'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
github: problame
|
||||||
|
patreon: zrepl
|
||||||
|
liberapay: zrepl
|
||||||
|
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
|
ko_fi: problame
|
||||||
-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"
|
|
||||||
Generated
-1038
File diff suppressed because it is too large
Load Diff
-67
@@ -1,67 +0,0 @@
|
|||||||
ignored = [
|
|
||||||
"github.com/inconshreveable/mousetrap",
|
|
||||||
]
|
|
||||||
|
|
||||||
required = [
|
|
||||||
"golang.org/x/tools/cmd/stringer",
|
|
||||||
"github.com/alvaroloes/enumer",
|
|
||||||
"github.com/golangci/golangci-lint/cmd/golangci-lint",
|
|
||||||
"golang.org/x/tools/cmd/goimports",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/jinzhu/copier"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/kr/pretty"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
version = "0.8.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
version = "1.1.4"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "github.com/zrepl/yaml-config"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/go-logfmt/logfmt"
|
|
||||||
version = "*"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/problame/go-netssh"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/prometheus/client_golang"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/golang/protobuf"
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/fatih/color"
|
|
||||||
version = "1.7.0"
|
|
||||||
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/gdamore/tcell"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "google.golang.org/grpc"
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
version = "1.1.0"
|
|
||||||
name = "github.com/google/uuid"
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
.PHONY: generate build test vet cover release docs docs-clean clean vendordeps format lint
|
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
||||||
.DEFAULT_GOAL := build
|
.PHONY: release bins-all release-noarch
|
||||||
|
.DEFAULT_GOAL := zrepl-bin
|
||||||
|
|
||||||
ARTIFACTDIR := artifacts
|
ARTIFACTDIR := artifacts
|
||||||
|
|
||||||
@@ -12,93 +13,325 @@ ifndef _ZREPL_VERSION
|
|||||||
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
GO := go
|
||||||
|
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||||
|
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"')
|
||||||
|
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
||||||
|
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_EXTRA_BUILDFLAGS :=
|
||||||
|
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
||||||
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
|
GOLANGCI_LINT := golangci-lint
|
||||||
|
GOCOVMERGE := gocovmerge
|
||||||
|
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.15
|
||||||
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||||
|
|
||||||
GO_BUILD := go build -ldflags $(GO_LDFLAGS)
|
ifneq ($(GOARM),)
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
|
else
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
|
||||||
|
endif
|
||||||
|
|
||||||
RELEASE_BINS := $(ARTIFACTDIR)/zrepl-freebsd-amd64
|
.PHONY: printvars
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-amd64
|
printvars:
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-darwin-amd64
|
@echo GOOS=$(GOOS)
|
||||||
|
@echo GOARCH=$(GOARCH)
|
||||||
RELEASE_NOARCH := $(ARTIFACTDIR)/zrepl-noarch.tar
|
@echo GOARM=$(GOARM)
|
||||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <(go env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
|
||||||
|
|
||||||
vendordeps:
|
|
||||||
dep ensure -v -vendor-only
|
|
||||||
|
|
||||||
generate: #not part of the build, must do that manually
|
|
||||||
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
|
||||||
go generate -x ./...
|
|
||||||
|
|
||||||
format:
|
|
||||||
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')
|
|
||||||
|
|
||||||
lint:
|
|
||||||
golangci-lint run ./...
|
|
||||||
|
|
||||||
build:
|
|
||||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl"
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
vet:
|
|
||||||
# for each supported platform to cover conditional compilation
|
|
||||||
GOOS=linux go vet ./...
|
|
||||||
GOOS=darwin go vet ./...
|
|
||||||
GOOS=freebsd go vet ./...
|
|
||||||
|
|
||||||
$(ARTIFACTDIR):
|
|
||||||
mkdir -p "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
|
||||||
mkdir -p "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion: $(RELEASE_BINS)
|
|
||||||
artifacts/$(THIS_PLATFORM_RELEASE_BIN) bashcomp "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/go_version.txt:
|
|
||||||
go version > $@
|
|
||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
|
||||||
make -C docs \
|
|
||||||
html \
|
|
||||||
BUILDDIR=../artifacts/docs \
|
|
||||||
|
|
||||||
docs-clean:
|
|
||||||
make -C docs \
|
|
||||||
clean \
|
|
||||||
BUILDDIR=../artifacts/docs
|
|
||||||
|
|
||||||
|
|
||||||
.PHONY: $(RELEASE_BINS)
|
##################### PRODUCING A RELEASE #############
|
||||||
# TODO: two wildcards possible
|
.PHONY: release wrapup-and-checksum check-git-clean sign clean
|
||||||
$(RELEASE_BINS): $(ARTIFACTDIR)/zrepl-%-amd64: generate $(ARTIFACTDIR) vet test lint
|
|
||||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
|
||||||
GOOS=$* GOARCH=amd64 $(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$*-amd64"
|
|
||||||
|
|
||||||
$(RELEASE_NOARCH): docs $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_version.txt
|
release: clean
|
||||||
|
# no cross-platform support for target test
|
||||||
|
$(MAKE) test-go
|
||||||
|
$(MAKE) bins-all
|
||||||
|
$(MAKE) noarch
|
||||||
|
$(MAKE) wrapup-and-checksum
|
||||||
|
$(MAKE) check-git-clean
|
||||||
|
ifeq (SIGN, 1)
|
||||||
|
$(make) sign
|
||||||
|
endif
|
||||||
|
@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
|
||||||
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
|
wrapup-and-checksum:
|
||||||
|
rm -f $(NOARCH_TARBALL)
|
||||||
tar --mtime='1970-01-01' --sort=name \
|
tar --mtime='1970-01-01' --sort=name \
|
||||||
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
||||||
-acf $@ \
|
--transform 's#dist#zrepl-$(_ZREPL_VERSION)-noarch/dist#' \
|
||||||
|
--transform 's#config/samples#zrepl-$(_ZREPL_VERSION)-noarch/config#' \
|
||||||
|
-acf $(NOARCH_TARBALL) \
|
||||||
$(ARTIFACTDIR)/docs/html \
|
$(ARTIFACTDIR)/docs/html \
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
$(ARTIFACTDIR)/go_version.txt
|
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||||
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
release: $(RELEASE_BINS) $(RELEASE_NOARCH)
|
dist \
|
||||||
|
config/samples
|
||||||
rm -rf "$(ARTIFACTDIR)/release"
|
rm -rf "$(ARTIFACTDIR)/release"
|
||||||
mkdir -p "$(ARTIFACTDIR)/release"
|
mkdir -p "$(ARTIFACTDIR)/release"
|
||||||
cp $^ "$(ARTIFACTDIR)/release"
|
cp -l $(ARTIFACTDIR)/zrepl-* \
|
||||||
|
$(ARTIFACTDIR)/platformtest-* \
|
||||||
|
"$(ARTIFACTDIR)/release"
|
||||||
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
||||||
|
|
||||||
|
check-git-clean:
|
||||||
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
||||||
@if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
@if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
||||||
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
||||||
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
||||||
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
||||||
|
git status; \
|
||||||
|
echo "git diff:"; \
|
||||||
|
git diff | cat; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi; \
|
fi; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
|
sign:
|
||||||
|
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||||
|
--armor \
|
||||||
|
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
|
||||||
|
|
||||||
clean: docs-clean
|
clean: docs-clean
|
||||||
rm -rf "$(ARTIFACTDIR)"
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
|
||||||
|
##################### BINARIES #####################
|
||||||
|
.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 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"; }}')
|
||||||
|
bins-all:
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
|
$(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=darwin GOARCH=amd64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
|
||||||
|
ifeq ($(GO_SUPPORTS_ILLUMOS), illumos)
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=illumos GOARCH=amd64
|
||||||
|
else ifeq ($(GO_SUPPORTS_ILLUMOS), noillumos)
|
||||||
|
@echo "SKIPPING ILLUMOS BUILD BECAUSE GO VERSION DOESN'T SUPPORT IT"
|
||||||
|
else
|
||||||
|
@echo "CANNOT DETERMINE WHETHER GO VERSION SUPPORTS GOOS=illumos"; exit 1
|
||||||
|
endif
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
$(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:
|
||||||
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||||
|
|
||||||
|
generate-platform-test-list:
|
||||||
|
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
||||||
|
|
||||||
|
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
||||||
|
cover-platform-bin:
|
||||||
|
$(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
|
||||||
|
|
||||||
|
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
|
||||||
|
test-platform-bin:
|
||||||
|
$(GO_BUILD) -o "$(TEST_PLATFORM_BIN_PATH)" ./platformtest/harness
|
||||||
|
test-platform:
|
||||||
|
export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \
|
||||||
|
$(MAKE) _test-or-cover-platform-impl
|
||||||
|
|
||||||
|
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||||
|
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||||
|
ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
||||||
|
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=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.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 #####################
|
||||||
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
|
|
||||||
|
|
||||||
|
$(ARTIFACTDIR):
|
||||||
|
mkdir -p "$@"
|
||||||
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
|
mkdir -p "$@"
|
||||||
|
|
||||||
|
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||||
|
# pass
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
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:
|
||||||
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
|
$(GO) version >> $@
|
||||||
|
|
||||||
|
docs: $(ARTIFACTDIR)/docs
|
||||||
|
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
|
||||||
|
make -C docs \
|
||||||
|
html \
|
||||||
|
BUILDDIR=../artifacts/docs \
|
||||||
|
SPHINXOPTS="-W --keep-going -n"
|
||||||
|
|
||||||
|
docs-clean:
|
||||||
|
make -C docs \
|
||||||
|
clean \
|
||||||
|
BUILDDIR=../artifacts/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,91 +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.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
|
||||||
|
An HTML report can be generated using `make cover-html`.
|
||||||
|
|
||||||
|
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
|
||||||
|
|
||||||
|
### Build & Release Process
|
||||||
|
|
||||||
|
**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 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.
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
|
||||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
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.
|
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||||
|
|
||||||
## Developer Documentation
|
|
||||||
|
|
||||||
zrepl is written in [Go](https://golang.org) and uses [`dep`](https://github.com/golang/dep) to manage dependencies.
|
## Contributing Code
|
||||||
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`.
|
|
||||||
|
|
||||||
### Overall Architecture
|
|
||||||
|
|
||||||
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
|
|
||||||
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── 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
|
|
||||||
|
|
||||||
* 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
|
||||||
@@ -113,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 />
|
||||||
@@ -132,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.
|
||||||
|
|
||||||
|
|||||||
+13
-14
@@ -1,12 +1,12 @@
|
|||||||
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
|
||||||
|
|
||||||
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
ADD build.installprotoc.bash ./
|
||||||
RUN echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
RUN bash build.installprotoc.bash
|
||||||
RUN unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
|
||||||
|
|
||||||
ADD lazy.sh /tmp/lazy.sh
|
ADD lazy.sh /tmp/lazy.sh
|
||||||
ADD docs/requirements.txt /tmp/requirements.txt
|
ADD docs/requirements.txt /tmp/requirements.txt
|
||||||
@@ -14,18 +14,17 @@ ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
|||||||
RUN /tmp/lazy.sh docdep
|
RUN /tmp/lazy.sh docdep
|
||||||
|
|
||||||
# prepare volume mount of git checkout to /zrepl
|
# prepare volume mount of git checkout to /zrepl
|
||||||
RUN mkdir -p /go/src/github.com/zrepl/zrepl
|
RUN mkdir -p /src/github.com/zrepl/zrepl
|
||||||
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
||||||
WORKDIR /go/src/github.com/zrepl/zrepl
|
|
||||||
|
|
||||||
ADD Gopkg.toml Gopkg.lock ./
|
# $GOPATH is /go
|
||||||
|
# Go 1.12 doesn't use modules within GOPATH, but 1.13 and later do
|
||||||
|
# => store source outside of GOPATH
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
# godep will install the Go dependencies to vendor in order to then build and install
|
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
||||||
# build dependencies like stringer to $GOPATH/bin.
|
ADD build/ /tmp/build
|
||||||
# However, since users volume-mount their Git checkout into /go/src/github.com/zrepl/zrepl
|
|
||||||
# the vendor directory will be empty at build time, allowing them to experiment with
|
|
||||||
# new checkouts, etc.
|
|
||||||
# Thus, we only use the vendored deps for building dependencies.
|
|
||||||
RUN /tmp/lazy.sh godep
|
RUN /tmp/lazy.sh godep
|
||||||
|
|
||||||
RUN chmod -R 0777 /go
|
RUN chmod -R 0777 /go
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
MACH=$(uname -m)
|
||||||
|
MACH="${MACH/aarch64/aarch_64}"
|
||||||
|
|
||||||
|
VERSION=3.6.1
|
||||||
|
FILENAME=protoc-"$VERSION"-linux-"$MACH".zip
|
||||||
|
|
||||||
|
if [ -e "$FILENAME" ]; then
|
||||||
|
echo "$FILENAME" already exists 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget https://github.com/protocolbuffers/protobuf/releases/download/v"$VERSION"/"$FILENAME"
|
||||||
|
|
||||||
|
stat "$FILENAME"
|
||||||
|
|
||||||
|
sha256sum -c --ignore-missing <<EOF
|
||||||
|
6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip
|
||||||
|
af8e5aaaf39ddec62ec8dd2be1b8d9602c6da66564883a16393ade5f71170922 protoc-3.6.1-linux-aarch_64.zip
|
||||||
|
EOF
|
||||||
|
|
||||||
|
unzip -d /usr "$FILENAME"
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
//+build windows
|
|
||||||
|
|
||||||
// This package cannot actually be built, and since the Travis
|
|
||||||
// tests run go test ./..., we have to avoid a build attempt.
|
|
||||||
// Windows is not supported atm, so this works ¯\_(ツ)_/¯
|
|
||||||
|
|
||||||
// This package is a pseudo-user of build-time dependencies for zrepl,
|
|
||||||
// mostly for various code-generation tools.
|
|
||||||
//
|
|
||||||
// The imports are necessary to satisfy go dep
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
_ "github.com/alvaroloes/enumer"
|
|
||||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("just a placeholder")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
module github.com/zrepl/zrepl/build
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
||||||
|
github.com/alvaroloes/enumer v1.1.1
|
||||||
|
github.com/fatih/color v1.7.0 // indirect
|
||||||
|
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/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/jwalterweatherman v1.1.0 // indirect
|
||||||
|
github.com/spf13/viper v1.3.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad // indirect
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||||
|
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
|
||||||
|
)
|
||||||
+282
@@ -0,0 +1,282 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
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/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 h1:pFks+oaqVWDFq0KsLQZFB2pQGB5Us0HfMSBENtieXOs=
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
|
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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
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/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
|
||||||
|
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
||||||
|
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||||
|
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-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||||
|
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||||
|
github.com/go-toolsmith/astcopy 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/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/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/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
||||||
|
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/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/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/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/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
|
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/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
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/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||||
|
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w=
|
||||||
|
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/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/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||||
|
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
|
||||||
|
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
||||||
|
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/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-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||||
|
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/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||||
|
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||||
|
github.com/golangci/misspell v0.3.4 h1:kAD5J0611Zo2VirUn9KFP15RjEqkmfEfnFxyIVxZCYg=
|
||||||
|
github.com/golangci/misspell v0.3.4/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||||
|
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
|
||||||
|
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM=
|
||||||
|
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/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||||
|
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.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
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/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||||
|
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
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/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
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/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
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.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/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.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
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-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/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||||
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||||
|
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
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/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
|
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
||||||
|
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
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/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
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/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.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/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/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
|
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
|
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/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/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
|
||||||
|
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||||
|
github.com/spf13/afero v1.1.0/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/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/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
|
||||||
|
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
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.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||||
|
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/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||||
|
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
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/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
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/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||||
|
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/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-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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-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-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
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/tools v0.0.0-20170915040203-e531a2a1c15f/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-20181205014116-22934f0fdb62/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-20190213192042-740235f6c0d8 h1:b0PLhFjEMqrIqsD4rS5sp0YxBYgdqKzX0KkkGGKLV8I=
|
||||||
|
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
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-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-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
|
||||||
|
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=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||||
|
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||||
|
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
|
||||||
|
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
||||||
|
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=
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// +build tools
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/alvaroloes/enumer"
|
||||||
|
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||||
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
|
_ "github.com/wadey/gocovmerge"
|
||||||
|
_ "golang.org/x/tools/cmd/goimports"
|
||||||
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
|
)
|
||||||
+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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+206
-22
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
@@ -11,7 +12,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
// tcell is the termbox-compatible 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/termbox"
|
||||||
|
_ "github.com/gdamore/tcell/terminfo/s/screen" // tmux on FreeBSD 11 & 12 without ncurses
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
@@ -20,6 +28,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon"
|
"github.com/zrepl/zrepl/daemon"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,9 +82,11 @@ type tui struct {
|
|||||||
indent int
|
indent int
|
||||||
|
|
||||||
lock sync.Mutex //For report and error
|
lock sync.Mutex //For report and error
|
||||||
report map[string]job.Status
|
report map[string]*job.Status
|
||||||
err error
|
err error
|
||||||
|
|
||||||
|
jobFilter string
|
||||||
|
|
||||||
replicationProgress map[string]*bytesProgressHistory // by job name
|
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +168,7 @@ func (t *tui) addIndent(indent int) {
|
|||||||
|
|
||||||
var statusFlags struct {
|
var statusFlags struct {
|
||||||
Raw bool
|
Raw bool
|
||||||
|
Job string
|
||||||
}
|
}
|
||||||
|
|
||||||
var StatusCmd = &cli.Subcommand{
|
var StatusCmd = &cli.Subcommand{
|
||||||
@@ -164,11 +176,12 @@ var StatusCmd = &cli.Subcommand{
|
|||||||
Short: "show job activity or dump as JSON for monitoring",
|
Short: "show job activity or dump as JSON for monitoring",
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||||
|
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||||
},
|
},
|
||||||
Run: runStatus,
|
Run: runStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStatus(s *cli.Subcommand, args []string) error {
|
func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
|
||||||
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -198,6 +211,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
t.lock.Lock()
|
t.lock.Lock()
|
||||||
t.err = errors.New("Got no report yet")
|
t.err = errors.New("Got no report yet")
|
||||||
t.lock.Unlock()
|
t.lock.Unlock()
|
||||||
|
t.jobFilter = statusFlags.Job
|
||||||
|
|
||||||
err = termbox.Init()
|
err = termbox.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -206,7 +220,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
defer termbox.Close()
|
defer termbox.Close()
|
||||||
|
|
||||||
update := func() {
|
update := func() {
|
||||||
m := make(map[string]job.Status)
|
var m daemon.Status
|
||||||
|
|
||||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||||
struct{}{},
|
struct{}{},
|
||||||
@@ -215,7 +229,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
t.lock.Lock()
|
t.lock.Lock()
|
||||||
t.err = err2
|
t.err = err2
|
||||||
t.report = m
|
t.report = m.Jobs
|
||||||
t.lock.Unlock()
|
t.lock.Unlock()
|
||||||
t.draw()
|
t.draw()
|
||||||
}
|
}
|
||||||
@@ -251,7 +265,7 @@ loop:
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
|
func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
|
||||||
p, ok := t.replicationProgress[jobName]
|
p, ok := t.replicationProgress[jobName]
|
||||||
if !ok {
|
if !ok {
|
||||||
p = &bytesProgressHistory{}
|
p = &bytesProgressHistory{}
|
||||||
@@ -273,19 +287,29 @@ func (t *tui) draw() {
|
|||||||
t.write(t.err.Error())
|
t.write(t.err.Error())
|
||||||
} else {
|
} else {
|
||||||
//Iterate over map in alphabetical order
|
//Iterate over map in alphabetical order
|
||||||
keys := make([]string, len(t.report))
|
keys := make([]string, 0, len(t.report))
|
||||||
i := 0
|
|
||||||
for k := range t.report {
|
for k := range t.report {
|
||||||
keys[i] = k
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
v := t.report[k]
|
|
||||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||||
continue
|
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.setIndent(0)
|
||||||
|
|
||||||
t.printf("Job: %s", k)
|
t.printf("Job: %s", k)
|
||||||
@@ -306,7 +330,7 @@ func (t *tui) draw() {
|
|||||||
t.printf("Replication:")
|
t.printf("Replication:")
|
||||||
t.newline()
|
t.newline()
|
||||||
t.addIndent(1)
|
t.addIndent(1)
|
||||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
|
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
|
||||||
t.addIndent(-1)
|
t.addIndent(-1)
|
||||||
|
|
||||||
t.printf("Pruning Sender:")
|
t.printf("Pruning Sender:")
|
||||||
@@ -320,6 +344,15 @@ func (t *tui) draw() {
|
|||||||
t.addIndent(1)
|
t.addIndent(1)
|
||||||
t.renderPrunerReport(activeStatus.PruningReceiver)
|
t.renderPrunerReport(activeStatus.PruningReceiver)
|
||||||
t.addIndent(-1)
|
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 {
|
} else if v.Type == job.TypeSnap {
|
||||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||||
if !ok || snapStatus == nil {
|
if !ok || snapStatus == nil {
|
||||||
@@ -332,6 +365,19 @@ func (t *tui) draw() {
|
|||||||
t.addIndent(1)
|
t.addIndent(1)
|
||||||
t.renderPrunerReport(snapStatus.Pruning)
|
t.renderPrunerReport(snapStatus.Pruning)
|
||||||
t.addIndent(-1)
|
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 {
|
} else {
|
||||||
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||||
t.newline()
|
t.newline()
|
||||||
@@ -384,7 +430,7 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
|
|||||||
} else {
|
} else {
|
||||||
t.printf("Attempt #%d", len(rep.Attempts))
|
t.printf("Attempt #%d", len(rep.Attempts))
|
||||||
if len(rep.Attempts) > 1 {
|
if len(rep.Attempts) > 1 {
|
||||||
t.printf(". Previous attempts failed with the follwing statuses:")
|
t.printf(". Previous attempts failed with the following statuses:")
|
||||||
t.newline()
|
t.newline()
|
||||||
t.addIndent(1)
|
t.addIndent(1)
|
||||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||||
@@ -416,12 +462,28 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
|
|||||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||||
// Draw global progress bar
|
// Draw global progress bar
|
||||||
// Progress: [---------------]
|
// Progress: [---------------]
|
||||||
expected, replicated := latest.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
rate, changeCount := history.Update(replicated)
|
rate, changeCount := history.Update(replicated)
|
||||||
|
eta := time.Duration(0)
|
||||||
|
if rate > 0 {
|
||||||
|
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||||
|
}
|
||||||
t.write("Progress: ")
|
t.write("Progress: ")
|
||||||
t.drawBar(50, replicated, expected, changeCount)
|
t.drawBar(50, replicated, expected, changeCount)
|
||||||
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||||
|
if eta != 0 {
|
||||||
|
t.write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||||
|
}
|
||||||
t.newline()
|
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
|
var maxFSLen int
|
||||||
for _, fs := range latest.Filesystems {
|
for _, fs := range latest.Filesystems {
|
||||||
@@ -545,6 +607,86 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func times(str string, n int) (out string) {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
out += str
|
out += str
|
||||||
@@ -556,12 +698,12 @@ func rightPad(str string, length int, pad string) string {
|
|||||||
if len(str) > length {
|
if len(str) > length {
|
||||||
return str[:length]
|
return str[:length]
|
||||||
}
|
}
|
||||||
return str + times(pad, length-len(str))
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
var arrowPositions = `>\|/`
|
var arrowPositions = `>\|/`
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progresss
|
// changeCount = 0 indicates stall / no progress
|
||||||
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||||
var completedLength int
|
var completedLength int
|
||||||
if totalBytes > 0 {
|
if totalBytes > 0 {
|
||||||
@@ -582,11 +724,20 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
|||||||
|
|
||||||
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||||
|
|
||||||
expected, replicated := rep.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)",
|
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)),
|
strings.ToUpper(string(rep.State)),
|
||||||
rep.CurrentStep, len(rep.Steps),
|
rep.CurrentStep, len(rep.Steps),
|
||||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||||
|
sizeEstimationImpreciseNotice,
|
||||||
)
|
)
|
||||||
|
|
||||||
activeIndicator := " "
|
activeIndicator := " "
|
||||||
@@ -606,8 +757,17 @@ func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, m
|
|||||||
if nextStep.IsIncremental() {
|
if nextStep.IsIncremental() {
|
||||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||||
} else {
|
} else {
|
||||||
next = fmt.Sprintf("next: %s (full)", nextStep.Info.To)
|
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 {
|
} else {
|
||||||
next = "" // individual FSes may still be in planning state
|
next = "" // individual FSes may still be in planning state
|
||||||
}
|
}
|
||||||
@@ -630,3 +790,27 @@ func ByteCountBinary(b int64) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, " ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
// print results
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
-18
@@ -52,11 +52,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 +76,48 @@ type SnapJob struct {
|
|||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendOptions struct {
|
||||||
|
Encrypted bool `yaml:"encrypted,optional,default=false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Replication struct {
|
||||||
|
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplicationOptionsProtection struct {
|
||||||
|
Initial string `yaml:"initial,optional,default=guarantee_resumability"`
|
||||||
|
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
|
||||||
|
}
|
||||||
|
|
||||||
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 +151,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 {
|
||||||
@@ -139,6 +179,7 @@ type SnapshottingPeriodic struct {
|
|||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Prefix string `yaml:"prefix"`
|
Prefix string `yaml:"prefix"`
|
||||||
Interval time.Duration `yaml:"interval,positive"`
|
Interval time.Duration `yaml:"interval,positive"`
|
||||||
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
type SnapshottingManual struct {
|
||||||
@@ -202,13 +243,13 @@ type ConnectCommon struct {
|
|||||||
|
|
||||||
type TCPConnect struct {
|
type TCPConnect struct {
|
||||||
ConnectCommon `yaml:",inline"`
|
ConnectCommon `yaml:",inline"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address,hostport"`
|
||||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSConnect struct {
|
type TLSConnect struct {
|
||||||
ConnectCommon `yaml:",inline"`
|
ConnectCommon `yaml:",inline"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address,hostport"`
|
||||||
Ca string `yaml:"ca"`
|
Ca string `yaml:"ca"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
@@ -230,8 +271,9 @@ type SSHStdinserverConnect struct {
|
|||||||
|
|
||||||
type LocalConnect struct {
|
type LocalConnect struct {
|
||||||
ConnectCommon `yaml:",inline"`
|
ConnectCommon `yaml:",inline"`
|
||||||
ListenerName string `yaml:"listener_name"`
|
ListenerName string `yaml:"listener_name"`
|
||||||
ClientIdentity string `yaml:"client_identity"`
|
ClientIdentity string `yaml:"client_identity"`
|
||||||
|
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=2s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServeEnum struct {
|
type ServeEnum struct {
|
||||||
@@ -243,14 +285,16 @@ type ServeCommon struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TCPServe struct {
|
type TCPServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen"`
|
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"`
|
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"`
|
||||||
@@ -280,6 +324,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
|
||||||
@@ -312,7 +357,7 @@ type SyslogLoggingOutlet struct {
|
|||||||
|
|
||||||
type TCPLoggingOutlet struct {
|
type TCPLoggingOutlet struct {
|
||||||
LoggingOutletCommon `yaml:",inline"`
|
LoggingOutletCommon `yaml:",inline"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address,hostport"`
|
||||||
Net string `yaml:"net,default=tcp"`
|
Net string `yaml:"net,default=tcp"`
|
||||||
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
||||||
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
|
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
|
||||||
@@ -329,8 +374,9 @@ type MonitoringEnum struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrometheusMonitoring struct {
|
type PrometheusMonitoring struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyslogFacility syslog.Priority
|
type SyslogFacility syslog.Priority
|
||||||
@@ -361,6 +407,38 @@ type JobDebugSettings struct {
|
|||||||
RPCLog bool `yaml:"rpc_log,optional,default=false"`
|
RPCLog bool `yaml:"rpc_log,optional,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HookList []HookEnum
|
||||||
|
|
||||||
|
type HookEnum struct {
|
||||||
|
Ret interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookCommand struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||||
|
Filesystems FilesystemsFilter `yaml:"filesystems,optional,default={'<': true}"`
|
||||||
|
HookSettingsCommon `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookPostgresCheckpoint struct {
|
||||||
|
HookSettingsCommon `yaml:",inline"`
|
||||||
|
DSN string `yaml:"dsn"`
|
||||||
|
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||||
|
Filesystems FilesystemsFilter `yaml:"filesystems"` // required, user should not CHECKPOINT for every FS
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookMySQLLockTables struct {
|
||||||
|
HookSettingsCommon `yaml:",inline"`
|
||||||
|
DSN string `yaml:"dsn"`
|
||||||
|
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||||
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookSettingsCommon struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
ErrIsFatal bool `yaml:"err_is_fatal,optional,default=false"`
|
||||||
|
}
|
||||||
|
|
||||||
func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) {
|
func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) {
|
||||||
var in struct {
|
var in struct {
|
||||||
Type string
|
Type string
|
||||||
@@ -501,6 +579,15 @@ func (t *SyslogFacility) UnmarshalYAML(u func(interface{}, bool) error) (err err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *HookEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||||
|
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||||
|
"command": &HookCommand{},
|
||||||
|
"postgres-checkpoint": &HookPostgresCheckpoint{},
|
||||||
|
"mysql-lock-tables": &HookMySQLLockTables{},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var ConfigFileDefaultLocations = []string{
|
var ConfigFileDefaultLocations = []string{
|
||||||
"/etc/zrepl/zrepl.yml",
|
"/etc/zrepl/zrepl.yml",
|
||||||
"/usr/local/etc/zrepl/zrepl.yml",
|
"/usr/local/etc/zrepl/zrepl.yml",
|
||||||
@@ -546,7 +633,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)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package config
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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
|
||||||
|
`
|
||||||
|
|
||||||
|
encrypted_unspecified := `
|
||||||
|
send: {}
|
||||||
|
`
|
||||||
|
|
||||||
|
send_not_specified := `
|
||||||
|
`
|
||||||
|
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
var c *Config
|
||||||
|
|
||||||
|
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("encrypted_unspecified", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fill(encrypted_unspecified))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, false, encrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
|
c, err := testConfig(t, fill(send_not_specified))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -38,6 +38,29 @@ jobs:
|
|||||||
interval: 10m
|
interval: 10m
|
||||||
`
|
`
|
||||||
|
|
||||||
|
hooks := `
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
interval: 10m
|
||||||
|
hooks:
|
||||||
|
- type: command
|
||||||
|
path: /tmp/path/to/command
|
||||||
|
- type: command
|
||||||
|
path: /tmp/path/to/command
|
||||||
|
filesystems: { "zroot<": true, "<": false }
|
||||||
|
- type: postgres-checkpoint
|
||||||
|
dsn: "host=localhost port=5432 user=postgres sslmode=disable"
|
||||||
|
filesystems: {
|
||||||
|
"tank/postgres/data11": true
|
||||||
|
}
|
||||||
|
- type: mysql-lock-tables
|
||||||
|
dsn: "root@tcp(localhost)/"
|
||||||
|
filesystems: {
|
||||||
|
"tank/mysql": true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
var c *Config
|
var c *Config
|
||||||
|
|
||||||
@@ -55,4 +78,13 @@ jobs:
|
|||||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("hooks", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fillSnapshotting(hooks))
|
||||||
|
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
|
||||||
|
assert.Equal(t, hs[0].Ret.(*HookCommand).Filesystems["<"], true)
|
||||||
|
assert.Equal(t, hs[1].Ret.(*HookCommand).Filesystems["zroot<"], true)
|
||||||
|
assert.Equal(t, hs[2].Ret.(*HookPostgresCheckpoint).Filesystems["tank/postgres/data11"], true)
|
||||||
|
assert.Equal(t, hs[3].Ret.(*HookMySQLLockTables).Filesystems["tank/mysql"], true)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-1
@@ -1,13 +1,17 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,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)
|
||||||
@@ -65,3 +69,20 @@ func testConfig(t *testing.T, input string) (*Config, error) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
return ParseConfigBytes([]byte(input))
|
return ParseConfigBytes([]byte(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trimSpaceEachLineAndPad(s, pad string) string {
|
||||||
|
var out strings.Builder
|
||||||
|
scan := bufio.NewScanner(strings.NewReader(s))
|
||||||
|
for scan.Scan() {
|
||||||
|
fmt.Fprintf(&out, "%s%s\n", pad, bytes.TrimSpace(scan.Bytes()))
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimSpaceEachLineAndPad(t *testing.T) {
|
||||||
|
foo := `
|
||||||
|
foo
|
||||||
|
bar baz
|
||||||
|
`
|
||||||
|
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransportConnect(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: foo
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
%s
|
||||||
|
filesystems: {"<": true}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
|
||||||
|
mconf := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
Name string
|
||||||
|
ExpectError bool
|
||||||
|
Connect string
|
||||||
|
}
|
||||||
|
|
||||||
|
testTable := []test{
|
||||||
|
{
|
||||||
|
Name: "tcp_with_address_and_port",
|
||||||
|
ExpectError: false,
|
||||||
|
Connect: `
|
||||||
|
type: tcp
|
||||||
|
address: 10.0.0.23:42
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tls_with_host_and_port",
|
||||||
|
ExpectError: false,
|
||||||
|
Connect: `
|
||||||
|
type: tls
|
||||||
|
address: "server1.foo.bar:8888"
|
||||||
|
ca: /etc/zrepl/ca.crt
|
||||||
|
cert: /etc/zrepl/backupserver.fullchain
|
||||||
|
key: /etc/zrepl/backupserver.key
|
||||||
|
server_cn: "server1"
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tcp_without_port",
|
||||||
|
ExpectError: true,
|
||||||
|
Connect: `
|
||||||
|
type: tcp
|
||||||
|
address: 10.0.0.23
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tls_without_port",
|
||||||
|
ExpectError: true,
|
||||||
|
Connect: `
|
||||||
|
type: tls
|
||||||
|
address: 10.0.0.23
|
||||||
|
ca: /etc/zrepl/ca.crt
|
||||||
|
cert: /etc/zrepl/backupserver.fullchain
|
||||||
|
key: /etc/zrepl/backupserver.key
|
||||||
|
server_cn: "server1"
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testTable {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
require.NotEmpty(t, tc.Connect)
|
||||||
|
connect := trimSpaceEachLineAndPad(tc.Connect, " ")
|
||||||
|
conf := mconf(connect)
|
||||||
|
config, err := testConfig(t, conf)
|
||||||
|
if tc.ExpectError && err == nil {
|
||||||
|
t.Errorf("expected test failure, but got valid config %v", config)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tc.ExpectError && err != nil {
|
||||||
|
t.Errorf("not expecint test failure but got error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("error=%v", err)
|
||||||
|
t.Logf("config=%v", config)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "$ZREPL_DRYRUN" = "true" ] && DRYRUN="echo DRYRUN: "
|
||||||
|
|
||||||
|
pre_snapshot() {
|
||||||
|
$DRYRUN date
|
||||||
|
}
|
||||||
|
|
||||||
|
post_snapshot() {
|
||||||
|
$DRYRUN date
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ZREPL_HOOKTYPE" in
|
||||||
|
pre_snapshot|post_snapshot)
|
||||||
|
"$ZREPL_HOOKTYPE"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'Unrecognized hook type: %s\n' "$ZREPL_HOOKTYPE"
|
||||||
|
exit 255
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
port: 22
|
port: 22
|
||||||
identity_file: /etc/zrepl/ssh/identity
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
options: # optional, default [], `-o` arguments passed to ssh
|
options: # optional, default [], `-o` arguments passed to ssh
|
||||||
- "Compression=on"
|
- "Compression=yes"
|
||||||
root_fs: "pool2/backup_servers"
|
root_fs: "pool2/backup_servers"
|
||||||
interval: 10m
|
interval: 10m
|
||||||
pruning:
|
pruning:
|
||||||
|
|||||||
@@ -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_derive`
|
||||||
|
# - 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,
|
||||||
|
|||||||
+22
-3
@@ -15,9 +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/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 {
|
||||||
@@ -43,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
|
||||||
@@ -60,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)
|
||||||
@@ -86,6 +91,12 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pprofServer := NewPProfServer(ctx)
|
pprofServer := NewPProfServer(ctx)
|
||||||
|
if listen := envconst.String("ZREPL_DAEMON_AUTOSTART_PPROF_SERVER", ""); listen != "" {
|
||||||
|
pprofServer.Control(PprofServerControlMsg{
|
||||||
|
Run: true,
|
||||||
|
HttpListenAddress: listen,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle(ControlJobEndpointPProf,
|
mux.Handle(ControlJobEndpointPProf,
|
||||||
@@ -107,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
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -240,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)
|
||||||
|
|||||||
+44
-16
@@ -13,6 +13,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 +24,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)
|
||||||
@@ -38,6 +42,7 @@ func Run(conf *config.Config) error {
|
|||||||
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 +52,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 +90,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 +114,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 +138,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 +218,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 +234,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 +243,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 {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDatasetMapFilter(t *testing.T) {
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
filter map[string]string
|
||||||
|
checkPass map[string]bool // each entry is checked to match the filter's `pass` return value
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []testCase{
|
||||||
|
{
|
||||||
|
"default_no_match",
|
||||||
|
map[string]string{},
|
||||||
|
map[string]bool{
|
||||||
|
"": false,
|
||||||
|
"foo": false,
|
||||||
|
"zroot": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"more_specific_path_has_precedence",
|
||||||
|
map[string]string{
|
||||||
|
"tank<": "ok",
|
||||||
|
"tank/tmp<": "!",
|
||||||
|
"tank/home/x<": "!",
|
||||||
|
"tank/home/x/1": "ok",
|
||||||
|
},
|
||||||
|
map[string]bool{
|
||||||
|
"zroot": false,
|
||||||
|
"tank": true,
|
||||||
|
"tank/tmp": false,
|
||||||
|
"tank/tmp/foo": false,
|
||||||
|
"tank/home/x": false,
|
||||||
|
"tank/home/y": true,
|
||||||
|
"tank/home/x/1": true,
|
||||||
|
"tank/home/x/2": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"precedence_of_specific_over_subtree_wildcard_on_same_path",
|
||||||
|
map[string]string{
|
||||||
|
"tank/home/bob": "ok",
|
||||||
|
"tank/home/bob<": "!",
|
||||||
|
},
|
||||||
|
map[string]bool{
|
||||||
|
"tank/home/bob": true,
|
||||||
|
"tank/home/bob/downloads": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tc := range tcs {
|
||||||
|
t.Run(tcs[tc].name, func(t *testing.T) {
|
||||||
|
c := tcs[tc]
|
||||||
|
f := NewDatasetMapFilter(len(c.filter), true)
|
||||||
|
for p, a := range c.filter {
|
||||||
|
err := f.Add(p, a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("incorrect filter spec: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p, checkPass := range c.checkPass {
|
||||||
|
zp, err := zfs.NewDatasetPath(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("incorrect path spec: %s", err)
|
||||||
|
}
|
||||||
|
pass, err := f.Filter(zp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected filter error: %s", err)
|
||||||
|
}
|
||||||
|
ok := pass == checkPass
|
||||||
|
failstr := "OK"
|
||||||
|
if !ok {
|
||||||
|
failstr = "FAIL"
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
t.Logf("%-40q %5v (exp=%v act=%v)", p, failstr, checkPass, pass)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Code generated by "stringer -type=Edge"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package hooks
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[Pre-1]
|
||||||
|
_ = x[Callback-2]
|
||||||
|
_ = x[Post-4]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_Edge_name_0 = "PreCallback"
|
||||||
|
_Edge_name_1 = "Post"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_Edge_index_0 = [...]uint8{0, 3, 11}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i Edge) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _Edge_name_0[_Edge_index_0[i]:_Edge_index_0[i+1]]
|
||||||
|
case i == 4:
|
||||||
|
return _Edge_name_1
|
||||||
|
default:
|
||||||
|
return "Edge(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type List []Hook
|
||||||
|
|
||||||
|
func HookFromConfig(in config.HookEnum) (Hook, error) {
|
||||||
|
switch v := in.Ret.(type) {
|
||||||
|
case *config.HookCommand:
|
||||||
|
return NewCommandHook(v)
|
||||||
|
case *config.HookPostgresCheckpoint:
|
||||||
|
return PgChkptHookFromConfig(v)
|
||||||
|
case *config.HookMySQLLockTables:
|
||||||
|
return MyLockTablesFromConfig(v)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown hook type %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListFromConfig(in *config.HookList) (r *List, err error) {
|
||||||
|
hl := make(List, len(*in))
|
||||||
|
|
||||||
|
for i, h := range *in {
|
||||||
|
hl[i], err = HookFromConfig(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create hook #%d: %s", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &hl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l List) CopyFilteredForFilesystem(fs *zfs.DatasetPath) (ret List, err error) {
|
||||||
|
ret = make(List, 0, len(l))
|
||||||
|
|
||||||
|
for _, h := range l {
|
||||||
|
var passFilesystem bool
|
||||||
|
if passFilesystem, err = h.Filesystems().Filter(fs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if passFilesystem {
|
||||||
|
ret = append(ret, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Package hooks implements pre- and post snapshot hooks.
|
||||||
|
//
|
||||||
|
// Plan is a generic executor for ExpectStepReports before and after an activity specified in a callback.
|
||||||
|
// It provides a reporting facility that can be polled while the plan is executing to gather progress information.
|
||||||
|
//
|
||||||
|
// This package also provides all supported hook type implementations and abstractions around them.
|
||||||
|
//
|
||||||
|
// Use For Other Kinds Of ExpectStepReports
|
||||||
|
//
|
||||||
|
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
|
||||||
|
//
|
||||||
|
// The Hook interface requires a hook to provide a Filesystems() filter, which doesn't make sense for
|
||||||
|
// all kinds of activities.
|
||||||
|
//
|
||||||
|
// The hook implementations should move out of this package.
|
||||||
|
// However, there is a lot of tight coupling which to untangle isn't worth it ATM.
|
||||||
|
//
|
||||||
|
// How This Package Is Used By Package Snapper
|
||||||
|
//
|
||||||
|
// Deserialize a config.List using ListFromConfig().
|
||||||
|
// Then it MUST filter the list to only contain hooks for a particular filesystem using
|
||||||
|
// hooksList.CopyFilteredForFilesystem(fs).
|
||||||
|
//
|
||||||
|
// Then create a CallbackHook using NewCallbackHookForFilesystem().
|
||||||
|
//
|
||||||
|
// Pass all of the above to NewPlan() which provides a Report() and Run() method:
|
||||||
|
//
|
||||||
|
// Plan.Run(ctx context.Context,dryRun bool) executes the plan and take a context as argument that should contain a logger added using hooks.WithLogger()).
|
||||||
|
// The value of dryRun is passed through to the hooks' Run() method.
|
||||||
|
// Command hooks make it available in the environment variable ZREPL_DRYRUN.
|
||||||
|
//
|
||||||
|
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
|
||||||
|
//
|
||||||
|
package hooks
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Re-export type here so that
|
||||||
|
// every file in package hooks doesn't
|
||||||
|
// have to import github.com/zrepl/zrepl/zfs
|
||||||
|
type Filter zfs.DatasetFilter
|
||||||
|
|
||||||
|
type Hook interface {
|
||||||
|
Filesystems() Filter
|
||||||
|
|
||||||
|
// If true and the Pre edge invocation of Run fails, Post edge will not run and other Pre edges will not run.
|
||||||
|
ErrIsFatal() bool
|
||||||
|
|
||||||
|
// Run is invoked by HookPlan for a Pre edge.
|
||||||
|
// If HookReport.HadError() == false, the Post edge will be invoked, too.
|
||||||
|
Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport
|
||||||
|
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Phase string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PhaseSnapshot = Phase("snapshot")
|
||||||
|
PhaseTesting = Phase("testing")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Phase) String() string {
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate stringer -type=Edge
|
||||||
|
type Edge uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
Pre = Edge(1 << iota)
|
||||||
|
Callback
|
||||||
|
Post
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e Edge) StringForPhase(phase Phase) string {
|
||||||
|
return fmt.Sprintf("%s_%s", e.String(), phase.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=StepStatus -trimprefix=Step
|
||||||
|
type StepStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StepPending StepStatus = 1 << iota
|
||||||
|
StepExec
|
||||||
|
StepOk
|
||||||
|
StepErr
|
||||||
|
StepSkippedDueToFatalErr
|
||||||
|
StepSkippedDueToPreErr
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookReport interface {
|
||||||
|
String() string
|
||||||
|
HadError() bool
|
||||||
|
Error() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Step struct {
|
||||||
|
Hook Hook
|
||||||
|
Edge Edge
|
||||||
|
Status StepStatus
|
||||||
|
Begin, End time.Time
|
||||||
|
// Report may be nil
|
||||||
|
// FIXME cannot serialize this for client status, but contains interesting info (like what error happened)
|
||||||
|
Report HookReport
|
||||||
|
state map[interface{}]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Step) String() (out string) {
|
||||||
|
fatal := "~"
|
||||||
|
if s.Hook.ErrIsFatal() && s.Edge == Pre {
|
||||||
|
fatal = "!"
|
||||||
|
}
|
||||||
|
runTime := "..."
|
||||||
|
if s.Status != StepPending {
|
||||||
|
t := s.End.Sub(s.Begin)
|
||||||
|
runTime = t.Round(time.Millisecond).String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s] [%5s] %s [%s] %s", s.Status, runTime, fatal, s.Edge, s.Hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plan struct {
|
||||||
|
mtx sync.RWMutex
|
||||||
|
|
||||||
|
steps []*Step
|
||||||
|
pre []*Step // protected by mtx
|
||||||
|
cb *Step
|
||||||
|
post []*Step // not reversed, i.e. entry at index i corresponds to pre-edge in pre[i]
|
||||||
|
|
||||||
|
phase Phase
|
||||||
|
env Env
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlan(hooks *List, phase Phase, cb *CallbackHook, extra Env) (*Plan, error) {
|
||||||
|
|
||||||
|
var pre, post []*Step
|
||||||
|
// TODO sanity check unique name of hook?
|
||||||
|
for _, hook := range *hooks {
|
||||||
|
state := make(map[interface{}]interface{})
|
||||||
|
preE := &Step{
|
||||||
|
Hook: hook,
|
||||||
|
Edge: Pre,
|
||||||
|
Status: StepPending,
|
||||||
|
state: state,
|
||||||
|
}
|
||||||
|
pre = append(pre, preE)
|
||||||
|
postE := &Step{
|
||||||
|
Hook: hook,
|
||||||
|
Edge: Post,
|
||||||
|
Status: StepPending,
|
||||||
|
state: state,
|
||||||
|
}
|
||||||
|
post = append(post, postE)
|
||||||
|
}
|
||||||
|
|
||||||
|
cbE := &Step{
|
||||||
|
Hook: cb,
|
||||||
|
Edge: Callback,
|
||||||
|
Status: StepPending,
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := make([]*Step, 0, len(pre)+len(post)+1)
|
||||||
|
steps = append(steps, pre...)
|
||||||
|
steps = append(steps, cbE)
|
||||||
|
for i := len(post) - 1; i >= 0; i-- {
|
||||||
|
steps = append(steps, post[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := &Plan{
|
||||||
|
phase: phase,
|
||||||
|
env: extra,
|
||||||
|
steps: steps,
|
||||||
|
pre: pre,
|
||||||
|
post: post,
|
||||||
|
cb: cbE,
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanReport []Step
|
||||||
|
|
||||||
|
func (p *Plan) Report() PlanReport {
|
||||||
|
p.mtx.RLock()
|
||||||
|
defer p.mtx.RUnlock()
|
||||||
|
rep := make([]Step, len(p.steps))
|
||||||
|
for i := range rep {
|
||||||
|
rep[i] = *p.steps[i]
|
||||||
|
}
|
||||||
|
return rep
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PlanReport) HadError() bool {
|
||||||
|
for _, e := range r {
|
||||||
|
if e.Status == StepErr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PlanReport) HadFatalError() bool {
|
||||||
|
for _, e := range r {
|
||||||
|
if e.Status == StepSkippedDueToFatalErr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PlanReport) String() string {
|
||||||
|
stepStrings := make([]string, len(r))
|
||||||
|
for i, e := range r {
|
||||||
|
stepStrings[i] = fmt.Sprintf("%02d %s", i+1, e)
|
||||||
|
}
|
||||||
|
return strings.Join(stepStrings, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plan) Run(ctx context.Context, dryRun bool) {
|
||||||
|
p.mtx.RLock()
|
||||||
|
defer p.mtx.RUnlock()
|
||||||
|
w := func(f func()) {
|
||||||
|
p.mtx.RUnlock()
|
||||||
|
defer p.mtx.RLock()
|
||||||
|
p.mtx.Lock()
|
||||||
|
defer p.mtx.Unlock()
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
runHook := func(s *Step, ctx context.Context, edge Edge) HookReport {
|
||||||
|
w(func() { s.Status = StepExec })
|
||||||
|
begin := time.Now()
|
||||||
|
r := s.Hook.Run(ctx, edge, p.phase, dryRun, p.env, s.state)
|
||||||
|
end := time.Now()
|
||||||
|
w(func() {
|
||||||
|
s.Report = r
|
||||||
|
s.Status = StepOk
|
||||||
|
if r.HadError() {
|
||||||
|
s.Status = StepErr
|
||||||
|
}
|
||||||
|
s.Begin, s.End = begin, end
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
l := getLogger(ctx)
|
||||||
|
|
||||||
|
// it's a stack, execute until we reach the end of the list (last item in)
|
||||||
|
// or fail inbetween
|
||||||
|
l.Info("run pre-edges in configuration order")
|
||||||
|
next := 0
|
||||||
|
for ; next < len(p.pre); next++ {
|
||||||
|
e := p.pre[next]
|
||||||
|
l := l.WithField("hook", e.Hook)
|
||||||
|
r := runHook(e, ctx, Pre)
|
||||||
|
if r.HadError() {
|
||||||
|
l.WithError(r).Error("hook invocation failed for pre-edge")
|
||||||
|
if e.Hook.ErrIsFatal() {
|
||||||
|
l.Error("the hook run was aborted due to a fatal error in this hook")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hadFatalErr := next != len(p.pre)
|
||||||
|
if hadFatalErr {
|
||||||
|
l.Error("fatal error in a pre-snapshot hook invocation")
|
||||||
|
l.Error("no snapshot will be taken")
|
||||||
|
l.Error("only running post-edges for successful pre-edges")
|
||||||
|
w(func() {
|
||||||
|
p.post[next].Status = StepSkippedDueToFatalErr
|
||||||
|
for i := next + 1; i < len(p.pre); i++ {
|
||||||
|
p.pre[i].Status = StepSkippedDueToFatalErr
|
||||||
|
p.post[i].Status = StepSkippedDueToFatalErr
|
||||||
|
}
|
||||||
|
p.cb.Status = StepSkippedDueToFatalErr
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info("running callback")
|
||||||
|
cbR := runHook(p.cb, ctx, Callback)
|
||||||
|
if cbR.HadError() {
|
||||||
|
l.WithError(cbR).Error("callback failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info("run post-edges for successful pre-edges in reverse configuration order")
|
||||||
|
|
||||||
|
// the constructor produces pre and post entries
|
||||||
|
// post is NOT reversed
|
||||||
|
next-- // now at index of last executed pre-edge
|
||||||
|
for ; next >= 0; next-- {
|
||||||
|
e := p.post[next]
|
||||||
|
l := l.WithField("hook", e.Hook)
|
||||||
|
|
||||||
|
if p.pre[next].Status != StepOk {
|
||||||
|
if p.pre[next].Status != StepErr {
|
||||||
|
panic(fmt.Sprintf("expecting a pre-edge hook report to be either Ok or Err, got %s", p.pre[next].Status))
|
||||||
|
}
|
||||||
|
l.Info("skip post-edge because pre-edge failed")
|
||||||
|
w(func() {
|
||||||
|
e.Status = StepSkippedDueToPreErr
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
report := runHook(e, ctx, Post)
|
||||||
|
|
||||||
|
if report.HadError() {
|
||||||
|
l.WithError(report).Error("hook invocation failed for post-edge")
|
||||||
|
l.Error("subsequent post-edges run regardless of this post-edge failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrIsFatal is only relevant for Pre
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger = logger.Logger
|
||||||
|
|
||||||
|
func GetLogger(ctx context.Context) Logger { return getLogger(ctx) }
|
||||||
|
|
||||||
|
func getLogger(ctx context.Context) Logger {
|
||||||
|
return logging.GetLogger(ctx, logging.SubsysHooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
||||||
|
|
||||||
|
type logWriter struct {
|
||||||
|
/*
|
||||||
|
Mutex prevents:
|
||||||
|
concurrent writes to buf, scanner in Write([]byte)
|
||||||
|
data race on scanner vs Write([]byte)
|
||||||
|
and concurrent write to buf (call to buf.Reset())
|
||||||
|
in Close()
|
||||||
|
|
||||||
|
(Also, Close() should generally block until any Write() call completes.)
|
||||||
|
*/
|
||||||
|
mtx *sync.Mutex
|
||||||
|
buf bytes.Buffer
|
||||||
|
scanner *bufio.Scanner
|
||||||
|
logger Logger
|
||||||
|
level logger.Level
|
||||||
|
field string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogWriter(mtx *sync.Mutex, logger Logger, level logger.Level, field string) *logWriter {
|
||||||
|
w := new(logWriter)
|
||||||
|
w.mtx = mtx
|
||||||
|
w.scanner = bufio.NewScanner(&w.buf)
|
||||||
|
w.logger = logger
|
||||||
|
w.level = level
|
||||||
|
w.field = field
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) log(line string) {
|
||||||
|
w.logger.WithField(w.field, line).Log(w.level, "hook output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) logUnreadBytes() error {
|
||||||
|
for w.scanner.Scan() {
|
||||||
|
w.log(w.scanner.Text())
|
||||||
|
}
|
||||||
|
if w.buf.Cap() > envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT) {
|
||||||
|
w.buf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) Write(in []byte) (int, error) {
|
||||||
|
w.mtx.Lock()
|
||||||
|
defer w.mtx.Unlock()
|
||||||
|
|
||||||
|
n, bufErr := w.buf.Write(in)
|
||||||
|
if bufErr != nil {
|
||||||
|
return n, bufErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.logUnreadBytes()
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
// Always reset the scanner for the next Write
|
||||||
|
w.scanner = bufio.NewScanner(&w.buf)
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) Close() (err error) {
|
||||||
|
w.mtx.Lock()
|
||||||
|
defer w.mtx.Unlock()
|
||||||
|
|
||||||
|
return w.logUnreadBytes()
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookJobCallback func(ctx context.Context) error
|
||||||
|
|
||||||
|
type CallbackHook struct {
|
||||||
|
cb HookJobCallback
|
||||||
|
filter Filter
|
||||||
|
displayString string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCallbackHookForFilesystem(displayString string, fs *zfs.DatasetPath, cb HookJobCallback) *CallbackHook {
|
||||||
|
filter, _ := filters.DatasetMapFilterFromConfig(map[string]bool{fs.ToString(): true})
|
||||||
|
return NewCallbackHook(displayString, cb, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCallbackHook(displayString string, cb HookJobCallback, filter Filter) *CallbackHook {
|
||||||
|
return &CallbackHook{
|
||||||
|
cb: cb,
|
||||||
|
filter: filter,
|
||||||
|
displayString: displayString,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) Filesystems() Filter {
|
||||||
|
return h.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) ErrIsFatal() bool {
|
||||||
|
return false // callback is by definition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) String() string {
|
||||||
|
return h.displayString
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallbackHookReport struct {
|
||||||
|
Name string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CallbackHookReport) String() string {
|
||||||
|
if r.HadError() {
|
||||||
|
return r.Error()
|
||||||
|
}
|
||||||
|
return r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CallbackHookReport) HadError() bool { return r.Err != nil }
|
||||||
|
|
||||||
|
func (r *CallbackHookReport) Error() string {
|
||||||
|
return fmt.Sprintf("%s error: %s", r.Name, r.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
err := h.cb(ctx)
|
||||||
|
return &CallbackHookReport{h.displayString, err}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/circlog"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookEnvVar string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvType HookEnvVar = "ZREPL_HOOKTYPE"
|
||||||
|
EnvDryRun HookEnvVar = "ZREPL_DRYRUN"
|
||||||
|
EnvFS HookEnvVar = "ZREPL_FS"
|
||||||
|
EnvSnapshot HookEnvVar = "ZREPL_SNAPNAME"
|
||||||
|
EnvTimeout HookEnvVar = "ZREPL_TIMEOUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Env map[HookEnvVar]string
|
||||||
|
|
||||||
|
func NewHookEnv(edge Edge, phase Phase, dryRun bool, timeout time.Duration, extra Env) Env {
|
||||||
|
r := Env{
|
||||||
|
EnvTimeout: fmt.Sprintf("%.f", math.Floor(timeout.Seconds())),
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeString := edge.StringForPhase(phase)
|
||||||
|
r[EnvType] = strings.ToLower(edgeString)
|
||||||
|
|
||||||
|
var dryRunString string
|
||||||
|
if dryRun {
|
||||||
|
dryRunString = "true"
|
||||||
|
} else {
|
||||||
|
dryRunString = ""
|
||||||
|
}
|
||||||
|
r[EnvDryRun] = dryRunString
|
||||||
|
|
||||||
|
for k, v := range extra {
|
||||||
|
r[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandHook struct {
|
||||||
|
edge Edge
|
||||||
|
filter Filter
|
||||||
|
errIsFatal bool
|
||||||
|
command string
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandHookReport struct {
|
||||||
|
Command string
|
||||||
|
Args []string // currently always empty
|
||||||
|
Env Env
|
||||||
|
Err error
|
||||||
|
CapturedStdoutStderrCombined []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CommandHookReport) String() string {
|
||||||
|
// Reproduces a POSIX shell-compatible command line
|
||||||
|
var cmdLine strings.Builder
|
||||||
|
sep := ""
|
||||||
|
|
||||||
|
// Make sure environment variables are always
|
||||||
|
// printed in the same order
|
||||||
|
var hookEnvKeys []HookEnvVar
|
||||||
|
for k := range r.Env {
|
||||||
|
hookEnvKeys = append(hookEnvKeys, k)
|
||||||
|
}
|
||||||
|
sort.Slice(hookEnvKeys, func(i, j int) bool { return string(hookEnvKeys[i]) < string(hookEnvKeys[j]) })
|
||||||
|
|
||||||
|
for _, k := range hookEnvKeys {
|
||||||
|
cmdLine.WriteString(fmt.Sprintf("%s%s='%s'", sep, k, r.Env[k]))
|
||||||
|
sep = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdLine.WriteString(fmt.Sprintf("%s%s", sep, r.Command))
|
||||||
|
for _, a := range r.Args {
|
||||||
|
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
|
||||||
|
}
|
||||||
|
func (r *CommandHookReport) Error() string {
|
||||||
|
if r.Err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s FAILED with error: %s", r.String(), r.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CommandHookReport) HadError() bool {
|
||||||
|
return r.Err != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandHook(in *config.HookCommand) (r *CommandHook, err error) {
|
||||||
|
r = &CommandHook{
|
||||||
|
errIsFatal: in.ErrIsFatal,
|
||||||
|
command: in.Path,
|
||||||
|
timeout: in.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.filter, err = filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse filesystem filter: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.edge = Pre | Post
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) Filesystems() Filter {
|
||||||
|
return h.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) ErrIsFatal() bool {
|
||||||
|
return h.errIsFatal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) String() string {
|
||||||
|
return h.command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
l := getLogger(ctx).WithField("command", h.command)
|
||||||
|
|
||||||
|
cmdCtx, cancel := context.WithTimeout(ctx, h.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmdExec := exec.CommandContext(cmdCtx, h.command)
|
||||||
|
|
||||||
|
hookEnv := NewHookEnv(edge, phase, dryRun, h.timeout, extra)
|
||||||
|
cmdEnv := os.Environ()
|
||||||
|
for k, v := range hookEnv {
|
||||||
|
cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
cmdExec.Env = cmdEnv
|
||||||
|
|
||||||
|
var scanMutex sync.Mutex
|
||||||
|
combinedOutput, err := circlog.NewCircularLog(envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT))
|
||||||
|
if err != nil {
|
||||||
|
return &CommandHookReport{Err: err}
|
||||||
|
}
|
||||||
|
logErrWriter := NewLogWriter(&scanMutex, l, logger.Warn, "stderr")
|
||||||
|
logOutWriter := NewLogWriter(&scanMutex, l, logger.Info, "stdout")
|
||||||
|
defer logErrWriter.Close()
|
||||||
|
defer logOutWriter.Close()
|
||||||
|
|
||||||
|
cmdExec.Stderr = io.MultiWriter(logErrWriter, combinedOutput)
|
||||||
|
cmdExec.Stdout = io.MultiWriter(logOutWriter, combinedOutput)
|
||||||
|
|
||||||
|
report := &CommandHookReport{
|
||||||
|
Command: h.command,
|
||||||
|
Env: hookEnv,
|
||||||
|
// no report.Args
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdExec.Start()
|
||||||
|
if err != nil {
|
||||||
|
report.Err = err
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdExec.Wait()
|
||||||
|
combinedOutputBytes := combinedOutput.Bytes()
|
||||||
|
report.CapturedStdoutStderrCombined = make([]byte, len(combinedOutputBytes))
|
||||||
|
copy(report.CapturedStdoutStderrCombined, combinedOutputBytes)
|
||||||
|
if err != nil {
|
||||||
|
if cmdCtx.Err() == context.DeadlineExceeded {
|
||||||
|
report.Err = fmt.Errorf("timed out after %s: %s", h.timeout, err)
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
report.Err = err
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
sqldriver "database/sql/driver"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
||||||
|
//
|
||||||
|
// Making Backups Using a File System Snapshot:
|
||||||
|
//
|
||||||
|
// If you are using a Veritas file system, you can make a backup like this:
|
||||||
|
//
|
||||||
|
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
||||||
|
// From another shell, execute mount vxfs snapshot.
|
||||||
|
// From the first client, execute UNLOCK TABLES.
|
||||||
|
// Copy files from the snapshot.
|
||||||
|
// Unmount the snapshot.
|
||||||
|
//
|
||||||
|
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
||||||
|
type MySQLLockTables struct {
|
||||||
|
errIsFatal bool
|
||||||
|
connector sqldriver.Connector
|
||||||
|
filesystems Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
type myLockTablesStateKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
myLockTablesConnection myLockTablesStateKey = 1 + iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func MyLockTablesFromConfig(in *config.HookMySQLLockTables) (*MySQLLockTables, error) {
|
||||||
|
conf, err := mysql.ParseDSN(in.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`dsn` invalid")
|
||||||
|
}
|
||||||
|
cn, err := mysql.NewConnector(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`connect` invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`filesystems` invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MySQLLockTables{
|
||||||
|
in.ErrIsFatal,
|
||||||
|
cn,
|
||||||
|
filesystems,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) ErrIsFatal() bool { return h.errIsFatal }
|
||||||
|
func (h *MySQLLockTables) Filesystems() Filter { return h.filesystems }
|
||||||
|
func (h *MySQLLockTables) String() string { return "MySQL FLUSH TABLES WITH READ LOCK" }
|
||||||
|
|
||||||
|
type MyLockTablesReport struct {
|
||||||
|
What string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MyLockTablesReport) HadError() bool { return r.Err != nil }
|
||||||
|
func (r *MyLockTablesReport) Error() string { return r.String() }
|
||||||
|
func (r *MyLockTablesReport) String() string {
|
||||||
|
var s strings.Builder
|
||||||
|
s.WriteString(r.What)
|
||||||
|
if r.Err != nil {
|
||||||
|
fmt.Fprintf(&s, ": %s", r.Err)
|
||||||
|
}
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
fs, ok := extra[EnvFS]
|
||||||
|
if !ok {
|
||||||
|
panic(extra)
|
||||||
|
}
|
||||||
|
dp, err := zfs.NewDatasetPath(fs)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if pass, err := h.filesystems.Filter(dp); err != nil {
|
||||||
|
return &MyLockTablesReport{What: "filesystem filter", Err: err}
|
||||||
|
} else if !pass {
|
||||||
|
getLogger(ctx).Debug("filesystem does not match filter, skipping")
|
||||||
|
return &MyLockTablesReport{What: "filesystem filter skipped this filesystem", Err: nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch edge {
|
||||||
|
case Pre:
|
||||||
|
err := h.doRunPre(ctx, dp, dryRun, state)
|
||||||
|
return &MyLockTablesReport{"FLUSH TABLES WITH READ LOCK", err}
|
||||||
|
case Post:
|
||||||
|
err := h.doRunPost(ctx, dp, dryRun, state)
|
||||||
|
return &MyLockTablesReport{"UNLOCK TABLES", err}
|
||||||
|
}
|
||||||
|
return &MyLockTablesReport{What: "skipped this edge", Err: nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) (err error) {
|
||||||
|
db := sql.OpenDB(h.connector)
|
||||||
|
defer func(err *error) {
|
||||||
|
if *err != nil {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
}(&err)
|
||||||
|
|
||||||
|
getLogger(ctx).Debug("do FLUSH TABLES WITH READ LOCK")
|
||||||
|
_, err = db.ExecContext(ctx, "FLUSH TABLES WITH READ LOCK")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state[myLockTablesConnection] = db
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) doRunPost(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) error {
|
||||||
|
|
||||||
|
db := state[myLockTablesConnection].(*sql.DB)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
getLogger(ctx).Debug("do UNLOCK TABLES")
|
||||||
|
_, err := db.ExecContext(ctx, "UNLOCK TABLES")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PgChkptHook struct {
|
||||||
|
errIsFatal bool
|
||||||
|
connector *pq.Connector
|
||||||
|
filesystems Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func PgChkptHookFromConfig(in *config.HookPostgresCheckpoint) (*PgChkptHook, error) {
|
||||||
|
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`filesystems` invalid")
|
||||||
|
}
|
||||||
|
cn, err := pq.NewConnector(in.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`dsn` invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PgChkptHook{
|
||||||
|
in.ErrIsFatal,
|
||||||
|
cn,
|
||||||
|
filesystems,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PgChkptHook) ErrIsFatal() bool { return h.errIsFatal }
|
||||||
|
func (h *PgChkptHook) Filesystems() Filter { return h.filesystems }
|
||||||
|
func (h *PgChkptHook) String() string { return "postgres checkpoint" }
|
||||||
|
|
||||||
|
type PgChkptHookReport struct{ Err error }
|
||||||
|
|
||||||
|
func (r *PgChkptHookReport) HadError() bool { return r.Err != nil }
|
||||||
|
func (r *PgChkptHookReport) Error() string { return r.Err.Error() }
|
||||||
|
func (r *PgChkptHookReport) String() string {
|
||||||
|
if r.Err != nil {
|
||||||
|
return fmt.Sprintf("postgres CHECKPOINT failed: %s", r.Err)
|
||||||
|
} else {
|
||||||
|
return "postgres CHECKPOINT completed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PgChkptHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
if edge != Pre {
|
||||||
|
return &PgChkptHookReport{nil}
|
||||||
|
}
|
||||||
|
fs, ok := extra[EnvFS]
|
||||||
|
if !ok {
|
||||||
|
panic(extra)
|
||||||
|
}
|
||||||
|
dp, err := zfs.NewDatasetPath(fs)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = h.doRunPre(ctx, dp, dryRun)
|
||||||
|
return &PgChkptHookReport{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PgChkptHook) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool) error {
|
||||||
|
|
||||||
|
if pass, err := h.filesystems.Filter(fs); err != nil || !pass {
|
||||||
|
getLogger(ctx).Debug("filesystem does not match filter, skipping")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := sql.OpenDB(h.connector)
|
||||||
|
defer db.Close()
|
||||||
|
dl, ok := ctx.Deadline()
|
||||||
|
if ok {
|
||||||
|
timeout := uint64(math.Floor(time.Until(dl).Seconds() * 1000)) // TODO go1.13 milliseconds
|
||||||
|
getLogger(ctx).WithField("statement_timeout", timeout).Debug("setting statement timeout for CHECKPOINT")
|
||||||
|
_, err := db.ExecContext(ctx, "SET statement_timeout TO ?", timeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dry {
|
||||||
|
getLogger(ctx).Debug("dry-run - use ping instead of CHECKPOINT")
|
||||||
|
return db.PingContext(ctx)
|
||||||
|
}
|
||||||
|
getLogger(ctx).Info("execute CHECKPOINT command")
|
||||||
|
_, err := db.ExecContext(ctx, "CHECKPOINT")
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
package hooks_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type comparisonAssertionFunc func(require.TestingT, interface{}, interface{}, ...interface{})
|
||||||
|
type valueAssertionFunc func(require.TestingT, interface{}, ...interface{})
|
||||||
|
|
||||||
|
type expectStep struct {
|
||||||
|
ExpectedEdge hooks.Edge
|
||||||
|
ExpectStatus hooks.StepStatus
|
||||||
|
OutputTest valueAssertionFunc
|
||||||
|
ErrorTest valueAssertionFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
Name string
|
||||||
|
Config []string
|
||||||
|
IsSlow bool
|
||||||
|
SuppressOutput bool
|
||||||
|
|
||||||
|
ExpectCallbackSkipped bool
|
||||||
|
ExpectHadFatalErr bool
|
||||||
|
ExpectHadError bool
|
||||||
|
ExpectStepReports []expectStep
|
||||||
|
}
|
||||||
|
|
||||||
|
func curryLeft(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
|
||||||
|
return curry(f, expected, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func curryRight(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
|
||||||
|
return curry(f, expected, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func curry(f comparisonAssertionFunc, expected interface{}, right bool) (ret valueAssertionFunc) {
|
||||||
|
ret = func(t require.TestingT, s interface{}, v ...interface{}) {
|
||||||
|
var x interface{}
|
||||||
|
var y interface{}
|
||||||
|
if right {
|
||||||
|
x = s
|
||||||
|
y = expected
|
||||||
|
} else {
|
||||||
|
x = expected
|
||||||
|
y = s
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v) > 0 {
|
||||||
|
f(t, x, y, v)
|
||||||
|
} else {
|
||||||
|
f(t, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHooks(t *testing.T) {
|
||||||
|
ctx, end := trace.WithTaskFromStack(context.Background())
|
||||||
|
defer end()
|
||||||
|
|
||||||
|
testFSName := "testpool/testdataset"
|
||||||
|
testSnapshotName := "testsnap"
|
||||||
|
|
||||||
|
tmpl, err := template.New("TestHooks").Parse(`
|
||||||
|
jobs:
|
||||||
|
- name: TestHooks
|
||||||
|
type: snap
|
||||||
|
filesystems: {"<": true}
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
interval: 1m
|
||||||
|
prefix: zrepl_snapjob_
|
||||||
|
hooks:
|
||||||
|
{{- template "List" . }}
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
regexpTest := func(s string) valueAssertionFunc {
|
||||||
|
return curryLeft(require.Regexp, regexp.MustCompile(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
containsTest := func(s string) valueAssertionFunc {
|
||||||
|
return curryRight(require.Contains, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
testTable := []testCase{
|
||||||
|
testCase{
|
||||||
|
Name: "no_hooks",
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
Name: "timeout",
|
||||||
|
IsSlow: true,
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-timeout.sh, timeout: 2s}`},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s ZREPL_TIMEOUT=2", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest(`timed out after 2(.\d+)?s`),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
Name: "check_env",
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
|
||||||
|
ExpectHadError: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "nonfatal_pre_error_continues",
|
||||||
|
ExpectCallbackSkipped: false,
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-error.sh}`},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr, // post-edge is not executed for failing pre-edge
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "pre_error_fatal_skips_subsequent_pre_edges_and_callback_and_its_post_edge_and_post_edges",
|
||||||
|
ExpectCallbackSkipped: true,
|
||||||
|
ExpectHadFatalErr: true,
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true}`,
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||||
|
},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepSkippedDueToFatalErr},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "post_error_fails_are_ignored_even_if_fatal",
|
||||||
|
ExpectHadFatalErr: false, // only occurs during Post, so it's not a fatal error
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-post-error.sh, err_is_fatal: true}`,
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||||
|
},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
// No-action run of test-post-error.sh
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: require.Empty,
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
// Pre run of test-report-env.sh
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "cleanup_check_env",
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "pre_error_cancels_post",
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`},
|
||||||
|
ExpectHadError: true,
|
||||||
|
ExpectHadFatalErr: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "pre_error_does_not_cancel_other_posts_but_itself",
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`,
|
||||||
|
},
|
||||||
|
ExpectHadError: true,
|
||||||
|
ExpectHadFatalErr: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "exceed_buffer_limit",
|
||||||
|
SuppressOutput: true,
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-large-stdout.sh}`},
|
||||||
|
ExpectHadError: false,
|
||||||
|
ExpectHadFatalErr: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: func(t require.TestingT, s interface{}, v ...interface{}) {
|
||||||
|
require.Len(t, s, 1<<20)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
// No-action run of above hook
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: require.Empty,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
Following not intended to test functionality of
|
||||||
|
filter package. Only to demonstrate that hook
|
||||||
|
filters are being applied. The following should
|
||||||
|
result in NO hooks running. If it does run, a
|
||||||
|
fatal hooks.RunReport will be returned.
|
||||||
|
*/
|
||||||
|
testCase{
|
||||||
|
Name: "exclude_all_filesystems",
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true, filesystems: {"<": false}}`,
|
||||||
|
},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Callback,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
parseHookConfig := func(t *testing.T, in string) *config.Config {
|
||||||
|
t.Helper()
|
||||||
|
conf, err := config.ParseConfigBytes([]byte(in))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, conf)
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
fillHooks := func(tt *testCase) string {
|
||||||
|
// make hook path absolute
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic("os.Getwd() failed")
|
||||||
|
}
|
||||||
|
var hooksTmpl string = "\n"
|
||||||
|
for _, l := range tt.Config {
|
||||||
|
hooksTmpl += fmt.Sprintf(" - %s\n", l)
|
||||||
|
}
|
||||||
|
tmpl.New("List").Parse(hooksTmpl)
|
||||||
|
|
||||||
|
var outBytes bytes.Buffer
|
||||||
|
data := struct {
|
||||||
|
WorkDir string
|
||||||
|
}{
|
||||||
|
WorkDir: cwd,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(&outBytes, data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outBytes.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var c *config.Config
|
||||||
|
fs, err := zfs.NewDatasetPath(testFSName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
log := logger.NewTestLogger(t)
|
||||||
|
|
||||||
|
var cbReached bool
|
||||||
|
cb := hooks.NewCallbackHookForFilesystem("testcallback", fs, func(_ context.Context) error {
|
||||||
|
cbReached = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
hookEnvExtra := hooks.Env{
|
||||||
|
hooks.EnvFS: fs.ToString(),
|
||||||
|
hooks.EnvSnapshot: testSnapshotName,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testTable {
|
||||||
|
if testing.Short() && tt.IsSlow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
c = parseHookConfig(t, fillHooks(&tt))
|
||||||
|
snp := c.Jobs[0].Ret.(*config.SnapJob).Snapshotting.Ret.(*config.SnapshottingPeriodic)
|
||||||
|
hookList, err := hooks.ListFromConfig(&snp.Hooks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
filteredHooks, err := hookList.CopyFilteredForFilesystem(fs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
plan, err := hooks.NewPlan(&filteredHooks, hooks.PhaseTesting, cb, hookEnvExtra)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("REPORT PRE EXECUTION:\n%s", plan.Report())
|
||||||
|
|
||||||
|
cbReached = false
|
||||||
|
|
||||||
|
if testing.Verbose() && !tt.SuppressOutput {
|
||||||
|
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
|
||||||
|
}
|
||||||
|
plan.Run(ctx, false)
|
||||||
|
report := plan.Report()
|
||||||
|
|
||||||
|
t.Logf("REPORT POST EXECUTION:\n%s", report)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TEST ASSERTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
t.Logf("len(runReports)=%v", len(report))
|
||||||
|
t.Logf("len(tt.ExpectStepReports)=%v", len(tt.ExpectStepReports))
|
||||||
|
require.Equal(t, len(tt.ExpectStepReports), len(report), "ExpectStepReports must be same length as expected number of hook runs, excluding possible Callback")
|
||||||
|
|
||||||
|
// Check if callback ran, when required
|
||||||
|
if tt.ExpectCallbackSkipped {
|
||||||
|
require.False(t, cbReached, "callback ran but should not have run")
|
||||||
|
} else {
|
||||||
|
require.True(t, cbReached, "callback should have run but did not")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a fatal run error occurred and was expected
|
||||||
|
require.Equal(t, tt.ExpectHadFatalErr, report.HadFatalError(), "non-matching HadFatalError")
|
||||||
|
require.Equal(t, tt.ExpectHadError, report.HadError(), "non-matching HadError")
|
||||||
|
|
||||||
|
if tt.ExpectHadFatalErr {
|
||||||
|
require.True(t, tt.ExpectHadError, "ExpectHadFatalErr implies ExpectHadError")
|
||||||
|
}
|
||||||
|
if !tt.ExpectHadError {
|
||||||
|
require.False(t, tt.ExpectHadFatalErr, "!ExpectHadError implies !ExpectHadFatalErr")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through each expected hook run
|
||||||
|
for i, hook := range tt.ExpectStepReports {
|
||||||
|
t.Logf("expecting report conforming to %v", hook)
|
||||||
|
|
||||||
|
exp, act := hook.ExpectStatus, report[i].Status
|
||||||
|
require.Equal(t, exp, act, "%s != %s", exp, act)
|
||||||
|
|
||||||
|
// Check for required ExpectedEdge
|
||||||
|
require.NotZero(t, hook.ExpectedEdge, "each hook must have an ExpectedEdge")
|
||||||
|
require.Equal(t, hook.ExpectedEdge, report[i].Edge,
|
||||||
|
"incorrect edge: expected %q, actual %q", hook.ExpectedEdge.String(), report[i].Edge.String(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check for expected output
|
||||||
|
if hook.OutputTest != nil {
|
||||||
|
require.IsType(t, (*hooks.CommandHookReport)(nil), report[i].Report)
|
||||||
|
chr := report[i].Report.(*hooks.CommandHookReport)
|
||||||
|
hook.OutputTest(t, string(chr.CapturedStdoutStderrCombined))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for expected errors
|
||||||
|
if hook.ErrorTest != nil {
|
||||||
|
hook.ErrorTest(t, string(report[i].Report.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Code generated by "enumer -type=StepStatus -trimprefix=Step"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_StepStatusName_0 = "PendingExec"
|
||||||
|
_StepStatusName_1 = "Ok"
|
||||||
|
_StepStatusName_2 = "Err"
|
||||||
|
_StepStatusName_3 = "SkippedDueToFatalErr"
|
||||||
|
_StepStatusName_4 = "SkippedDueToPreErr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_StepStatusIndex_0 = [...]uint8{0, 7, 11}
|
||||||
|
_StepStatusIndex_1 = [...]uint8{0, 2}
|
||||||
|
_StepStatusIndex_2 = [...]uint8{0, 3}
|
||||||
|
_StepStatusIndex_3 = [...]uint8{0, 20}
|
||||||
|
_StepStatusIndex_4 = [...]uint8{0, 18}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i StepStatus) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _StepStatusName_0[_StepStatusIndex_0[i]:_StepStatusIndex_0[i+1]]
|
||||||
|
case i == 4:
|
||||||
|
return _StepStatusName_1
|
||||||
|
case i == 8:
|
||||||
|
return _StepStatusName_2
|
||||||
|
case i == 16:
|
||||||
|
return _StepStatusName_3
|
||||||
|
case i == 32:
|
||||||
|
return _StepStatusName_4
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("StepStatus(%d)", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _StepStatusValues = []StepStatus{1, 2, 4, 8, 16, 32}
|
||||||
|
|
||||||
|
var _StepStatusNameToValueMap = map[string]StepStatus{
|
||||||
|
_StepStatusName_0[0:7]: 1,
|
||||||
|
_StepStatusName_0[7:11]: 2,
|
||||||
|
_StepStatusName_1[0:2]: 4,
|
||||||
|
_StepStatusName_2[0:3]: 8,
|
||||||
|
_StepStatusName_3[0:20]: 16,
|
||||||
|
_StepStatusName_4[0:18]: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepStatusString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func StepStatusString(s string) (StepStatus, error) {
|
||||||
|
if val, ok := _StepStatusNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to StepStatus values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepStatusValues returns all values of the enum
|
||||||
|
func StepStatusValues() []StepStatus {
|
||||||
|
return _StepStatusValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAStepStatus returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i StepStatus) IsAStepStatus() bool {
|
||||||
|
for _, v := range _StepStatusValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
|
||||||
|
exit 1
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
# Exceed default hook log size of 1<<20 bytes by double
|
||||||
|
# The Go test should fail if buffer size exceeds 1MB
|
||||||
|
|
||||||
|
pre_testing() {
|
||||||
|
ln=0
|
||||||
|
while :; do printf '%06d: 012345678901234567890123456789012345678901234567890123456789012345678901\n' "$ln"; ln="$(($ln + 1))"; done \
|
||||||
|
| head -c$(( 2 << 20 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ZREPL_HOOKTYPE" in
|
||||||
|
pre_testing)
|
||||||
|
"$ZREPL_HOOKTYPE";;
|
||||||
|
*)
|
||||||
|
# Not handled by this script
|
||||||
|
exit 0;;
|
||||||
|
esac
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
post_testing() {
|
||||||
|
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ZREPL_HOOKTYPE" in
|
||||||
|
post_testing)
|
||||||
|
"$ZREPL_HOOKTYPE";;
|
||||||
|
*)
|
||||||
|
# Not handled by this script
|
||||||
|
exit 0;;
|
||||||
|
esac
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
if [ "$ZREPL_HOOKTYPE" = "pre_testing" ]; then
|
||||||
|
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
exit 1
|
||||||
|
elif [ "$ZREPL_HOOKTYPE" = "post_testing" ]; then
|
||||||
|
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
else
|
||||||
|
printf "Unknown hook type: %s" "$ZREPL_HOOKTYPE"
|
||||||
|
exit 255
|
||||||
|
fi
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ${ZREPL_DRYRUN:+(dry run)}"
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ZREPL_TIMEOUT=$ZREPL_TIMEOUT"
|
||||||
|
|
||||||
|
exec sleep $(($ZREPL_TIMEOUT + 1))
|
||||||
+156
-60
@@ -2,17 +2,19 @@ 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/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 +30,15 @@ import (
|
|||||||
|
|
||||||
type ActiveSide struct {
|
type ActiveSide struct {
|
||||||
mode activeMode
|
mode activeMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
connecter transport.Connecter
|
connecter transport.Connecter
|
||||||
|
|
||||||
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,30 +81,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
|
||||||
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() {
|
||||||
@@ -120,10 +126,16 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *modePush) SnapperReport() *snapper.Report {
|
||||||
|
return m.snapper.Report()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *modePush) ResetConnectBackoff() {
|
func (m *modePush) ResetConnectBackoff() {
|
||||||
m.setupMtx.Lock()
|
m.setupMtx.Lock()
|
||||||
defer m.setupMtx.Unlock()
|
defer m.setupMtx.Unlock()
|
||||||
@@ -132,15 +144,26 @@ 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,21 +171,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() {
|
||||||
@@ -181,6 +205,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")
|
||||||
@@ -206,6 +232,10 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *modePull) SnapperReport() *snapper.Report {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *modePull) ResetConnectBackoff() {
|
func (m *modePull) ResetConnectBackoff() {
|
||||||
m.setupMtx.Lock()
|
m.setupMtx.Lock()
|
||||||
defer m.setupMtx.Unlock()
|
defer m.setupMtx.Unlock()
|
||||||
@@ -214,40 +244,71 @@ 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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")
|
||||||
@@ -258,7 +319,7 @@ 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 {
|
||||||
@@ -272,13 +333,15 @@ 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
|
||||||
PruningSender, PruningReceiver *pruner.Report
|
PruningSender, PruningReceiver *pruner.Report
|
||||||
|
Snapshotting *snapper.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Status() *Status {
|
func (j *ActiveSide) Status() *Status {
|
||||||
@@ -295,6 +358,7 @@ func (j *ActiveSide) Status() *Status {
|
|||||||
if tasks.prunerReceiver != nil {
|
if tasks.prunerReceiver != nil {
|
||||||
s.PruningReceiver = tasks.prunerReceiver.Report()
|
s.PruningReceiver = tasks.prunerReceiver.Report()
|
||||||
}
|
}
|
||||||
|
s.Snapshotting = j.mode.SnapperReport()
|
||||||
return &Status{Type: t, JobSpecific: s}
|
return &Status{Type: t, JobSpecific: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,19 +368,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:
|
||||||
@@ -332,17 +420,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)
|
||||||
@@ -365,20 +451,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, 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -387,16 +479,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 {
|
||||||
@@ -404,16 +498,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)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT.
|
// Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -48,20 +48,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 +63,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 +88,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
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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/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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
Encrypt: &zfs.NilBool{B: in.GetSendOptions().Encrypted},
|
||||||
|
JobID: jobID,
|
||||||
|
}, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: rootFs,
|
||||||
|
AppendClientIdentity: in.GetAppendClientIdentity(),
|
||||||
|
}
|
||||||
|
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,98 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range paths {
|
||||||
|
|
||||||
|
if path.Ext(p) != ".yml" {
|
||||||
|
t.Logf("skipping file %s", p)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(p, func(t *testing.T) {
|
||||||
|
c, err := config.ParseConfig(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("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)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+76
-32
@@ -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,55 +21,56 @@ import (
|
|||||||
|
|
||||||
type PassiveSide struct {
|
type PassiveSide struct {
|
||||||
mode passiveMode
|
mode passiveMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
listen transport.AuthenticatedListenerFactory
|
listen transport.AuthenticatedListenerFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
type passiveMode interface {
|
type passiveMode interface {
|
||||||
Handler() rpc.Handler
|
Handler() rpc.Handler
|
||||||
RunPeriodic(ctx context.Context)
|
RunPeriodic(ctx context.Context)
|
||||||
|
SnapperReport() *snapper.Report // may be nil
|
||||||
Type() Type
|
Type() Type
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,16 +80,36 @@ 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) {
|
||||||
m.snapper.Run(ctx, nil)
|
m.snapper.Run(ctx, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
func (m *modeSource) SnapperReport() *snapper.Report {
|
||||||
|
return m.snapper.Report()
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
@@ -95,12 +117,17 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PassiveSide) Status() *Status {
|
func (s *PassiveSide) Status() *Status {
|
||||||
return &Status{Type: s.mode.Type()} // FIXME PassiveStatus
|
st := &PassiveStatus{
|
||||||
|
Snapper: s.mode.SnapperReport(),
|
||||||
|
}
|
||||||
|
return &Status{Type: s.mode.Type(), JobSpecific: st}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||||
@@ -109,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)
|
||||||
}
|
}
|
||||||
@@ -130,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
|
||||||
|
|||||||
+32
-16
@@ -8,10 +8,11 @@ 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/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 +21,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
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ type SnapJob struct {
|
|||||||
pruner *pruner.Pruner
|
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 +40,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 {
|
||||||
@@ -66,7 +70,8 @@ func (j *SnapJob) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SnapJobStatus struct {
|
type SnapJobStatus struct {
|
||||||
Pruning *pruner.Report
|
Pruning *pruner.Report
|
||||||
|
Snapshotting *snapper.Report // may be nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SnapJob) Status() *Status {
|
func (j *SnapJob) Status() *Status {
|
||||||
@@ -75,6 +80,7 @@ func (j *SnapJob) Status() *Status {
|
|||||||
if j.pruner != nil {
|
if j.pruner != nil {
|
||||||
s.Pruning = j.pruner.Report()
|
s.Pruning = j.pruner.Report()
|
||||||
}
|
}
|
||||||
|
s.Snapshotting = j.snapper.Report()
|
||||||
return &Status{Type: t, JobSpecific: s}
|
return &Status{Type: t, JobSpecific: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,16 +88,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:
|
||||||
@@ -106,8 +117,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +144,6 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
|||||||
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
var _ pruner.History = (*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(),
|
||||||
}
|
}
|
||||||
@@ -158,9 +168,15 @@ 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 encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||||
|
Encrypt: &zfs.NilBool{B: true},
|
||||||
|
})
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
log.Info("start pruning")
|
log.Info("start pruning")
|
||||||
j.pruner.Prune()
|
j.pruner.Prune()
|
||||||
|
|||||||
+101
-29
@@ -11,16 +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/pruner"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
"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) {
|
||||||
@@ -37,7 +30,7 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
|||||||
var syslogOutlets, stdoutOutlets int
|
var syslogOutlets, stdoutOutlets int
|
||||||
for lei, le := range in {
|
for lei, le := range in {
|
||||||
|
|
||||||
outlet, minLevel, err := parseOutlet(le)
|
outlet, minLevel, err := ParseOutlet(le)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
||||||
}
|
}
|
||||||
@@ -68,37 +61,116 @@ 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"
|
||||||
SubsysTransport Subsystem = "transport"
|
SubsysTransport Subsystem = "transport"
|
||||||
SubsysTransportMux Subsystem = "transportmux"
|
SubsysTransportMux Subsystem = "transportmux"
|
||||||
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 = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
SubsysSnapshot,
|
||||||
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
SubsysHooks,
|
||||||
ctx = rpc.WithLoggers(ctx,
|
SubsysTransport,
|
||||||
rpc.Loggers{
|
SubsysTransportMux,
|
||||||
General: log.WithField(SubsysField, SubsysRPC),
|
SubsysRPC,
|
||||||
Control: log.WithField(SubsysField, SubsysRPCControl),
|
SubsysRPCControl,
|
||||||
Data: log.WithField(SubsysField, SubsysRPCData),
|
SubsysRPCData,
|
||||||
},
|
SubsysZFSCmd,
|
||||||
)
|
SubsysTraceData,
|
||||||
return ctx
|
SubsysPlatformtest,
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -123,7 +195,7 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
func ParseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
||||||
|
|
||||||
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
|
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
|
||||||
if common.Level == "" || common.Format == "" {
|
if common.Level == "" || common.Format == "" {
|
||||||
|
|||||||
@@ -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,10 +86,10 @@ 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].(string)
|
val, ok := e.Fields[field]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -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,74 @@
|
|||||||
|
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
|
||||||
|
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)
|
||||||
|
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,16 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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)
|
||||||
|
}
|
||||||
@@ -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,47 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
var genIdPRNG = rand.New(rand.NewSource(1))
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
genIdPRNG.Seed(time.Now().UnixNano())
|
||||||
|
genIdPRNG.Seed(int64(os.Getpid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := genIdPRNG.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 {
|
||||||
|
|||||||
+17
-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,9 +66,10 @@ 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
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -80,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
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-17
@@ -12,19 +12,20 @@ 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
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type History interface {
|
type History 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
|
// 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,17 +36,13 @@ 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 {
|
||||||
@@ -138,7 +135,7 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *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,
|
receiver,
|
||||||
f.senderRules,
|
f.senderRules,
|
||||||
@@ -154,7 +151,7 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
|
|||||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *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,
|
receiver,
|
||||||
f.receiverRules,
|
f.receiverRules,
|
||||||
@@ -170,7 +167,7 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
ctx,
|
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||||
target,
|
target,
|
||||||
receiver,
|
receiver,
|
||||||
f.keepRules,
|
f.keepRules,
|
||||||
@@ -412,9 +409,6 @@ 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 := receiver.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Code generated by "enumer -type=State"; DO NOT EDIT.
|
// Code generated by "enumer -type=State"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
package pruner
|
package pruner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
+199
-89
@@ -9,9 +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/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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,27 +29,30 @@ const (
|
|||||||
SnapError
|
SnapError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// All fields protected by Snapper.mtx
|
||||||
type snapProgress struct {
|
type snapProgress struct {
|
||||||
state SnapState
|
state SnapState
|
||||||
|
|
||||||
// SnapStarted, SnapDone, SnapError
|
// SnapStarted, SnapDone, SnapError
|
||||||
name string
|
name string
|
||||||
startAt time.Time
|
startAt time.Time
|
||||||
|
hookPlan *hooks.Plan
|
||||||
|
|
||||||
// SnapDone
|
// SnapDone
|
||||||
doneAt time.Time
|
doneAt time.Time
|
||||||
|
|
||||||
// SnapErr
|
// SnapErr TODO disambiguate state
|
||||||
err error
|
runResults hooks.PlanReport
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Snapper struct {
|
type Snapper struct {
|
||||||
@@ -58,7 +65,7 @@ type Snapper struct {
|
|||||||
lastInvocation time.Time
|
lastInvocation time.Time
|
||||||
|
|
||||||
// valid for state Snapshotting
|
// valid for state Snapshotting
|
||||||
plan map[*zfs.DatasetPath]snapProgress
|
plan map[*zfs.DatasetPath]*snapProgress
|
||||||
|
|
||||||
// valid for state SyncUp and Waiting
|
// valid for state SyncUp and Waiting
|
||||||
sleepUntil time.Time
|
sleepUntil time.Time
|
||||||
@@ -96,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")
|
||||||
}
|
}
|
||||||
@@ -123,10 +117,16 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
|
|||||||
return nil, errors.New("interval must be positive")
|
return nil, errors.New("interval must be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hook config error")
|
||||||
|
}
|
||||||
|
|
||||||
args := args{
|
args := args{
|
||||||
prefix: in.Prefix,
|
prefix: in.Prefix,
|
||||||
interval: in.Interval,
|
interval: in.Interval,
|
||||||
fsf: fsf,
|
fsf: fsf,
|
||||||
|
hooks: hookList,
|
||||||
// ctx and log is set in Run()
|
// ctx and log is set in Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,13 +134,13 @@ 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
|
||||||
|
|
||||||
u := func(u func(*Snapper)) State {
|
u := func(u func(*Snapper)) State {
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
@@ -177,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,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)
|
||||||
}
|
}
|
||||||
@@ -224,54 +224,103 @@ func plan(a args, u updater) state {
|
|||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
plan := make(map[*zfs.DatasetPath]snapProgress, len(fss))
|
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||||
for _, fs := range fss {
|
for _, fs := range fss {
|
||||||
plan[fs] = snapProgress{state: SnapPending}
|
plan[fs] = &snapProgress{state: SnapPending}
|
||||||
}
|
}
|
||||||
return u(func(s *Snapper) {
|
return u(func(s *Snapper) {
|
||||||
s.state = Snapshotting
|
s.state = Snapshotting
|
||||||
s.plan = plan
|
s.plan = plan
|
||||||
|
s.err = nil
|
||||||
}).sf()
|
}).sf()
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshot(a args, u updater) state {
|
func snapshot(a args, u updater) state {
|
||||||
|
|
||||||
var plan map[*zfs.DatasetPath]snapProgress
|
var plan map[*zfs.DatasetPath]*snapProgress
|
||||||
u(func(snapper *Snapper) {
|
u(func(snapper *Snapper) {
|
||||||
plan = snapper.plan
|
plan = snapper.plan
|
||||||
})
|
})
|
||||||
|
|
||||||
hadErr := false
|
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
|
||||||
|
for _, h := range *a.hooks {
|
||||||
|
hookMatchCount[h] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
anyFsHadErr := false
|
||||||
// TODO channel programs -> allow a little jitter?
|
// TODO channel programs -> allow a little jitter?
|
||||||
for fs, progress := range plan {
|
for fs, progress := range plan {
|
||||||
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{
|
||||||
|
hooks.EnvFS: fs.ToString(),
|
||||||
|
hooks.EnvSnapshot: snapname,
|
||||||
|
}
|
||||||
|
|
||||||
|
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
||||||
|
l := getLogger(ctx)
|
||||||
|
l.Debug("create snapshot")
|
||||||
|
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Error("cannot create snapshot")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
fsHadErr := false
|
||||||
|
var planReport hooks.PlanReport
|
||||||
|
var plan *hooks.Plan
|
||||||
|
{
|
||||||
|
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
||||||
|
if err != nil {
|
||||||
|
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
||||||
|
fsHadErr = true
|
||||||
|
goto updateFSState
|
||||||
|
}
|
||||||
|
// account for running hooks
|
||||||
|
for _, h := range filteredHooks {
|
||||||
|
hookMatchCount[h] = hookMatchCount[h] + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var planErr error
|
||||||
|
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
||||||
|
if planErr != nil {
|
||||||
|
fsHadErr = true
|
||||||
|
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
||||||
|
goto updateFSState
|
||||||
|
}
|
||||||
|
}
|
||||||
u(func(snapper *Snapper) {
|
u(func(snapper *Snapper) {
|
||||||
progress.name = snapname
|
progress.name = snapname
|
||||||
progress.startAt = time.Now()
|
progress.startAt = time.Now()
|
||||||
|
progress.hookPlan = plan
|
||||||
progress.state = SnapStarted
|
progress.state = SnapStarted
|
||||||
})
|
})
|
||||||
|
{
|
||||||
l.Debug("create snapshot")
|
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
|
||||||
err := zfs.ZFSSnapshot(fs, snapname, false)
|
plan.Run(ctx, a.dryRun)
|
||||||
if err != nil {
|
planReport = plan.Report()
|
||||||
hadErr = true
|
fsHadErr = planReport.HadError() // not just fatal errors
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
if fsHadErr {
|
||||||
}
|
getLogger(ctx).WithField("report", planReport.String()).Error("end run job plan with error")
|
||||||
doneAt := time.Now()
|
} else {
|
||||||
|
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
progress.doneAt = doneAt
|
|
||||||
progress.state = SnapDone
|
|
||||||
if err != nil {
|
|
||||||
progress.state = SnapError
|
|
||||||
progress.err = err
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFSState:
|
||||||
|
anyFsHadErr = anyFsHadErr || fsHadErr
|
||||||
|
u(func(snapper *Snapper) {
|
||||||
|
progress.doneAt = time.Now()
|
||||||
|
progress.state = SnapDone
|
||||||
|
if fsHadErr {
|
||||||
|
progress.state = SnapError
|
||||||
|
}
|
||||||
|
progress.runResults = planReport
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,16 +328,30 @@ 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for h, mc := range hookMatchCount {
|
||||||
|
if mc == 0 {
|
||||||
|
hookIdx := -1
|
||||||
|
for idx, ah := range *a.hooks {
|
||||||
|
if ah == h {
|
||||||
|
hookIdx = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return u(func(snapper *Snapper) {
|
return u(func(snapper *Snapper) {
|
||||||
if hadErr {
|
if anyFsHadErr {
|
||||||
snapper.state = ErrorWait
|
snapper.state = ErrorWait
|
||||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||||
} else {
|
} else {
|
||||||
snapper.state = Waiting
|
snapper.state = Waiting
|
||||||
|
snapper.err = nil
|
||||||
}
|
}
|
||||||
}).sf()
|
}).sf()
|
||||||
}
|
}
|
||||||
@@ -299,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
|
||||||
@@ -320,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,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:
|
||||||
@@ -24,7 +24,15 @@ func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
// Returns nil if manual
|
||||||
|
func (s *PeriodicOrManual) Report() *Report {
|
||||||
|
if s.s != nil {
|
||||||
|
return s.s.Report()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Report struct {
|
||||||
|
State State
|
||||||
|
// valid in state SyncUp and Waiting
|
||||||
|
SleepUntil time.Time
|
||||||
|
// valid in state Err
|
||||||
|
Error string
|
||||||
|
// valid in state Snapshotting
|
||||||
|
Progress []*ReportFilesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportFilesystem struct {
|
||||||
|
Path string
|
||||||
|
State SnapState
|
||||||
|
|
||||||
|
// Valid in SnapStarted and later
|
||||||
|
SnapName string
|
||||||
|
StartAt time.Time
|
||||||
|
Hooks string
|
||||||
|
HooksHadError bool
|
||||||
|
|
||||||
|
// Valid in SnapDone | SnapError
|
||||||
|
DoneAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func errOrEmptyString(e error) string {
|
||||||
|
if e != nil {
|
||||||
|
return e.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snapper) Report() *Report {
|
||||||
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
|
pReps := make([]*ReportFilesystem, 0, len(s.plan))
|
||||||
|
for fs, p := range s.plan {
|
||||||
|
var hooksStr string
|
||||||
|
var hooksHadError bool
|
||||||
|
if p.hookPlan != nil {
|
||||||
|
hr := p.hookPlan.Report()
|
||||||
|
// FIXME: technically this belongs into client
|
||||||
|
// but we can't serialize hooks.Step ATM
|
||||||
|
rightPad := func(str string, length int, pad string) string {
|
||||||
|
if len(str) > length {
|
||||||
|
return str[:length]
|
||||||
|
}
|
||||||
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
|
}
|
||||||
|
hooksHadError = hr.HadError()
|
||||||
|
rows := make([][]string, len(hr))
|
||||||
|
const numCols = 4
|
||||||
|
lens := make([]int, numCols)
|
||||||
|
for i, e := range hr {
|
||||||
|
rows[i] = make([]string, numCols)
|
||||||
|
rows[i][0] = fmt.Sprintf("%d", i+1)
|
||||||
|
rows[i][1] = e.Status.String()
|
||||||
|
runTime := "..."
|
||||||
|
if e.Status != hooks.StepPending {
|
||||||
|
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
||||||
|
}
|
||||||
|
rows[i][2] = runTime
|
||||||
|
rows[i][3] = ""
|
||||||
|
if e.Report != nil {
|
||||||
|
rows[i][3] = e.Report.String()
|
||||||
|
}
|
||||||
|
for j, col := range lens {
|
||||||
|
if len(rows[i][j]) > col {
|
||||||
|
lens[j] = len(rows[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowsFlat := make([]string, len(hr))
|
||||||
|
for i, r := range rows {
|
||||||
|
colsPadded := make([]string, len(r))
|
||||||
|
for j, c := range r[:len(r)-1] {
|
||||||
|
colsPadded[j] = rightPad(c, lens[j], " ")
|
||||||
|
}
|
||||||
|
colsPadded[len(r)-1] = r[len(r)-1]
|
||||||
|
rowsFlat[i] = strings.Join(colsPadded, " ")
|
||||||
|
}
|
||||||
|
hooksStr = strings.Join(rowsFlat, "\n")
|
||||||
|
}
|
||||||
|
pReps = append(pReps, &ReportFilesystem{
|
||||||
|
Path: fs.ToString(),
|
||||||
|
State: p.state,
|
||||||
|
SnapName: p.name,
|
||||||
|
StartAt: p.startAt,
|
||||||
|
DoneAt: p.doneAt,
|
||||||
|
Hooks: hooksStr,
|
||||||
|
HooksHadError: hooksHadError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(pReps, func(i, j int) bool {
|
||||||
|
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
r := &Report{
|
||||||
|
State: s.state,
|
||||||
|
SleepUntil: s.sleepUntil,
|
||||||
|
Error: errOrEmptyString(s.err),
|
||||||
|
Progress: pReps,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user