Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c6d69a69c | |||
| 299808aaaf | |||
| 7071adc774 | |||
| 8d678eed19 | |||
| ab7abd9686 | |||
| a58ce74ed0 | |||
| 2c8c2cfa14 | |||
| 0ceea1b792 | |||
| 07f2bfff6a | |||
| b9ee14f6d7 | |||
| 393fc10a69 | |||
| 1c937e58f7 | |||
| 70bbdfe760 | |||
| efe7b17d21 | |||
| 166e80bb96 | |||
| 6ac537210b | |||
| 596a39c0f5 | |||
| d118bcc717 | |||
| 48be4032a2 | |||
| 2d545c87cd | |||
| 0b48ba54f8 | |||
| c3d87289bb | |||
| 0d96627ffb | |||
| 61acc7494a | |||
| 96d5288667 | |||
| b8cd3c59f1 | |||
| 91632b52bb | |||
| d39c0e3745 | |||
| 180eaea195 | |||
| 69ed2d7117 | |||
| c420f3c909 | |||
| 0a2dea05a9 | |||
| 293c89d392 | |||
| bb5ef0c8b2 | |||
| 8f44cdc284 | |||
| 53028ed50a | |||
| 17e152c601 | |||
| 2ba6aabd4a | |||
| 98207c904d | |||
| d2b825a8ae | |||
| 913b8b37fe | |||
| f494c5ba31 | |||
| b9503685f0 | |||
| 0f4143c0e0 | |||
| 8839ed1f95 | |||
| 41b4038ad5 | |||
| 88d21eb23a | |||
| e0be7e4d4f | |||
| 0c189265e8 | |||
| b1f8cdf385 | |||
| 428a60870a | |||
| af2d6579c5 | |||
| 0f3da73ef1 | |||
| fecc9416ab | |||
| a7915db4c3 | |||
| 3a4e841c73 | |||
| bcf6ff1c08 | |||
| ad7a104ab4 | |||
| 639359f393 | |||
| 91e310b7e3 | |||
| 5b30ad01ce | |||
| 8f4f9338a9 | |||
| 67bbce3c36 | |||
| 180c3d9ae1 | |||
| 7a5883d404 | |||
| c90acefacb | |||
| d6799e08d8 | |||
| c44dccc34b | |||
| 7f1695c457 | |||
| e500d9ee26 | |||
| d5ce578929 | |||
| 3f8fe3a368 | |||
| 6b4c6fc062 | |||
| e239d6f633 | |||
| 0bbe2befce | |||
| fa4e048169 | |||
| 4f9f21f7f2 | |||
| 480176ba2d | |||
| 1190c0f6d2 | |||
| 720a284db5 | |||
| 83fdffbcef | |||
| 0ee7a49d31 | |||
| 02db5994fe | |||
| 1d7a84e8ae | |||
| a0e3dc7040 | |||
| 43495d70c7 | |||
| fa586b493c | |||
| 30cdc1430e | |||
| 27673a23e9 | |||
| 95fc299733 | |||
| 4e702eedc9 | |||
| 8ff83f2f1a | |||
| 4b8f0ad112 | |||
| dbc8bbeb6a | |||
| b3e856f40d |
+346
-123
@@ -1,74 +1,79 @@
|
||||
version: 2.0
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build-1.11
|
||||
- build-1.12
|
||||
- build-1.13
|
||||
- build-1.14
|
||||
- build-latest
|
||||
- test-build-in-docker
|
||||
jobs:
|
||||
version: 2.1
|
||||
|
||||
# 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
|
||||
commands:
|
||||
setup-home-local-bin:
|
||||
steps:
|
||||
- run:
|
||||
name: Setup environment variables
|
||||
shell: /bin/bash -euo pipefail
|
||||
command: |
|
||||
# used by pip (for docs)
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
||||
# we use modules
|
||||
echo 'export GO111MODULE=on' >> $BASH_ENV
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
line='export PATH="$HOME/.local/bin:$PATH"'
|
||||
if grep "$line" $BASH_ENV >/dev/null; then
|
||||
echo "$line" >> $BASH_ENV
|
||||
fi
|
||||
|
||||
invoke-lazy-sh:
|
||||
parameters:
|
||||
subcommand:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
environment:
|
||||
TERM: xterm
|
||||
command: ./lazy.sh <<parameters.subcommand>>
|
||||
|
||||
apt-update-and-install-common-deps:
|
||||
steps:
|
||||
- run: sudo apt update && sudo apt install gawk make
|
||||
|
||||
restore-cache-gomod:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- source
|
||||
- protobuf
|
||||
|
||||
- checkout
|
||||
|
||||
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||
save-cache-gomod:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: source
|
||||
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||
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
|
||||
- "/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: |
|
||||
if which mc; then exit 0; fi
|
||||
sudo curl -sSL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2020-08-20T00-23-01Z \
|
||||
-o "$HOME/.local/bin/mc"
|
||||
sudo chmod +x "$HOME/.local/bin/mc"
|
||||
- save_cache:
|
||||
key: protobuf
|
||||
key: minio-client-v2
|
||||
paths:
|
||||
- "/usr/include/google/protobuf"
|
||||
|
||||
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev gawk
|
||||
- run: ./lazy.sh devsetup
|
||||
|
||||
- run: make zrepl-bin
|
||||
- run: make vet
|
||||
- run: make lint
|
||||
- run: make release
|
||||
- run: make test-go
|
||||
# cannot run test-platform because circle-ci runs in linux containers
|
||||
|
||||
- store_artifacts:
|
||||
path: ./artifacts/release
|
||||
when: always
|
||||
- "$HOME/.local/bin/mc"
|
||||
|
||||
upload-minio:
|
||||
parameters:
|
||||
src:
|
||||
type: string
|
||||
dst:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
shell: /bin/bash -eo pipefail
|
||||
when: always
|
||||
@@ -78,78 +83,296 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
set -u # from now on
|
||||
|
||||
# Download and install minio
|
||||
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
|
||||
chmod +x ${GOPATH}/bin/mc
|
||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||
|
||||
# Upload artifacts
|
||||
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
|
||||
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
|
||||
|
||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||
|
||||
# keep in sync with set-github-minio-status
|
||||
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||
|
||||
# Upload artifacts
|
||||
mkdir -p ./artifacts
|
||||
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
|
||||
|
||||
set-github-minio-status:
|
||||
parameters:
|
||||
context:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
minio-dst:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
shell: /bin/bash -eo pipefail
|
||||
command: |
|
||||
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
|
||||
exit 0
|
||||
fi
|
||||
set -u # from now on
|
||||
|
||||
# keep in sync with with upload-minio command
|
||||
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||
# Push Artifact Link to GitHub
|
||||
REPO="zrepl/zrepl"
|
||||
COMMIT="${CIRCLE_SHA1}"
|
||||
JOB_NAME="${CIRCLE_JOB}"
|
||||
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 "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
|
||||
-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"'"}'
|
||||
|
||||
- run:
|
||||
shell: /bin/bash -euo pipefail
|
||||
command: |
|
||||
# Trigger Debian Package Build
|
||||
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
||||
-H 'Accept: application/vnd.github.v3+json' \
|
||||
-H "Authorization: token $ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" \
|
||||
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$CIRCLE_SHA1"'", "go_version": "'"${CIRCLE_JOB##build-}"'" }}'
|
||||
|
||||
build-1.11:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
|
||||
build-1.12:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
|
||||
build-1.13:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.13
|
||||
|
||||
build-1.14:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.14
|
||||
|
||||
# this job tries to mimic the build-in-docker instructions
|
||||
# given in docs/installation.rst
|
||||
#
|
||||
# However, CircleCI doesn't support volume mounts, so we have to copy
|
||||
# the source into the build-container by modifying the Dockerfile
|
||||
test-build-in-docker:
|
||||
description: Check that build-in-docker works
|
||||
docker:
|
||||
- image: circleci/golang:latest
|
||||
environment:
|
||||
working_directory: /go/src/github.com/zrepl/zrepl
|
||||
trigger-pipeline:
|
||||
parameters:
|
||||
body_no_shell_subst:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: (hacky) circleci doesn't allow volume mounts, so copy src to container
|
||||
command: echo "ADD . /src" >> build.Dockerfile
|
||||
- run:
|
||||
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
|
||||
command: git -c user.name='circleci' -c user.email='circleci@localhost' commit -m 'CIRCLECI modified Dockerfile with zrepl src' --author 'autoauthor <circleci@localhost>' -- build.Dockerfile
|
||||
- run:
|
||||
name: build the build image (build deps)
|
||||
command: docker build -t zrepl_build -f build.Dockerfile .
|
||||
- run:
|
||||
name: try compiling
|
||||
command: docker run -it zrepl_build make release
|
||||
- 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.16"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
ci:
|
||||
when: << pipeline.parameters.do_ci >>
|
||||
jobs:
|
||||
- quickcheck-docs
|
||||
- quickcheck-go: &quickcheck-go-smoketest
|
||||
name: quickcheck-go-amd64-linux-1.16
|
||||
goversion: &latest-go-release "1.16"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- test-go-on-latest-go-release:
|
||||
goversion: *latest-go-release
|
||||
- quickcheck-go:
|
||||
requires:
|
||||
- quickcheck-go-amd64-linux-1.16 #quickcheck-go-smoketest.name
|
||||
matrix: &quickcheck-go-matrix
|
||||
alias: quickcheck-go-matrix
|
||||
parameters:
|
||||
goversion: [*latest-go-release, "1.12"]
|
||||
goos: ["linux", "freebsd"]
|
||||
goarch: ["amd64", "arm64"]
|
||||
exclude:
|
||||
# don't re-do quickcheck-go-smoketest
|
||||
- goversion: *latest-go-release
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
# not supported by Go 1.12
|
||||
- goversion: "1.12"
|
||||
goos: freebsd
|
||||
goarch: arm64
|
||||
|
||||
release:
|
||||
when: << pipeline.parameters.do_release >>
|
||||
jobs:
|
||||
- release-build
|
||||
- release-deb:
|
||||
requires:
|
||||
- release-build
|
||||
- release-rpm:
|
||||
requires:
|
||||
- release-build
|
||||
- release-upload:
|
||||
requires:
|
||||
- release-build
|
||||
- release-deb
|
||||
- release-rpm
|
||||
|
||||
periodic:
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "00 17 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- stable
|
||||
- problame/circleci-build
|
||||
jobs:
|
||||
- periodic-full-pipeline-run
|
||||
|
||||
zrepl.github.io:
|
||||
jobs:
|
||||
- publish-zrepl-github-io:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- stable
|
||||
|
||||
jobs:
|
||||
quickcheck-docs:
|
||||
docker:
|
||||
- image: cimg/base:2020.08
|
||||
steps:
|
||||
- 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:
|
||||
- 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"'" }}'
|
||||
-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"
|
||||
@@ -28,6 +28,8 @@ 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.16
|
||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||
|
||||
ifneq ($(GOARM),)
|
||||
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||
@@ -57,6 +59,85 @@ ifeq (SIGN, 1)
|
||||
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:
|
||||
@@ -147,26 +228,41 @@ zrepl-bin:
|
||||
generate-platform-test-list:
|
||||
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
||||
|
||||
test-platform-bin:
|
||||
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
||||
cover-platform-bin:
|
||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||
-c -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||
-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-platform: $(ARTIFACTDIR) # do not track dependency on test-platform-bin to allow build of platformtest outside of test VM
|
||||
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` \
|
||||
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||
-test.coverprofile "$(ARTIFACTDIR)/platformtest.cover" \
|
||||
-test.v \
|
||||
__DEVEL--i-heard-you-like-tests \
|
||||
$(_TEST_PLATFORM_CMD) \
|
||||
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||
@@ -178,22 +274,32 @@ cover-merge: $(ARTIFACTDIR)
|
||||
cover-html: cover-merge
|
||||
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
||||
|
||||
test-full:
|
||||
cover-full:
|
||||
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
||||
$(MAKE) test-go COVER=1
|
||||
$(MAKE) test-platform
|
||||
$(MAKE) cover-platform-bin
|
||||
$(MAKE) cover-platform
|
||||
$(MAKE) cover-html
|
||||
|
||||
##################### DEV TARGETS #####################
|
||||
# not part of the build, must do that manually
|
||||
.PHONY: generate format
|
||||
.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
|
||||
protoc -I=replication/logic/pdu --go_out=replication/logic/pdu --go-grpc_out=replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||
protoc -I=rpc/grpcclientidentity/example --go_out=rpc/grpcclientidentity/example/pdu --go-grpc_out=rpc/grpcclientidentity/example/pdu rpc/grpcclientidentity/example/grpcauth.proto
|
||||
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
||||
|
||||
GOIMPORTS := goimports -srcdir . -local 'github.com/zrepl/zrepl'
|
||||
FINDSRCFILES := find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go'
|
||||
|
||||
formatcheck:
|
||||
@# goimports doesn't have a knob to exit with non-zero status code if formatting is needed
|
||||
@# see https://go-review.googlesource.com/c/tools/+/237378
|
||||
@ affectedfiles=$$($(GOIMPORTS) -l $(shell $(FINDSRCFILES)) | tee /dev/stderr | wc -l); test "$$affectedfiles" = 0
|
||||
|
||||
format:
|
||||
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
|
||||
@ $(GOIMPORTS) -w -d $(shell $(FINDSRCFILES))
|
||||
|
||||
##################### NOARCH #####################
|
||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||
@@ -217,11 +323,14 @@ $(ARTIFACTDIR)/_zrepl.zsh_completion:
|
||||
|
||||
$(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 \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||
[](https://golang.org/)
|
||||
[](https://zrepl.github.io)
|
||||
[](https://www.patreon.com/zrepl)
|
||||
[](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)
|
||||
@@ -21,7 +21,7 @@ zrepl is a one-stop ZFS backup & replication solution.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
@@ -30,20 +30,12 @@ zrepl is a one-stop ZFS backup & replication solution.
|
||||
The above does not apply if you already implemented everything.
|
||||
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.
|
||||
* Make sure your distro is compatible with the paths in `docs/installation.rst`.
|
||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||
* `dist/systemd` contains a Systemd unit template.
|
||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||
* Use `sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||
This section provides an overview of the zrepl build & release process.
|
||||
Check out `docs/installation/compile-from-source.rst` for build-from-source instructions.
|
||||
|
||||
## Developer Documentation
|
||||
### Overview
|
||||
|
||||
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
||||
@@ -60,60 +52,61 @@ 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.
|
||||
|
||||
### Project Structure
|
||||
### Build & Release Process
|
||||
|
||||
```
|
||||
├── artifacts # build artifcats generate by make
|
||||
├── cli # wrapper around CLI package cobra
|
||||
├── client # all subcommands that are not `daemon`
|
||||
├── config # config data types (=> package yaml-config)
|
||||
│ └── samples
|
||||
├── daemon # the implementation of `zrepl daemon` subcommand
|
||||
│ ├── filters
|
||||
│ ├── hooks # snapshot hooks
|
||||
│ ├── job # job implementations
|
||||
│ ├── logging # logging outlets + formatters
|
||||
│ ├── nethelpers
|
||||
│ ├── prometheus
|
||||
│ ├── pruner # pruner implementation
|
||||
│ ├── snapper # snapshotter implementation
|
||||
├── dist # supplemental material for users & package maintainers
|
||||
├── docs # sphinx-based documentation
|
||||
│ ├── **/*.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
|
||||
├── platformtest # test suite for our zfs abstractions (error classification, etc)
|
||||
├── 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 # transport implementations
|
||||
│ ├── fromconfig
|
||||
│ ├── local
|
||||
│ ├── ssh
|
||||
│ ├── tcp
|
||||
│ └── tls
|
||||
├── util
|
||||
├── version # abstraction for versions (filled during build by Makefile)
|
||||
└── zfs # zfs(8) wrappers
|
||||
```
|
||||
**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.
|
||||
|
||||
### Coding Workflow
|
||||
**Build tooling & dependencies** are documented as code in `lazy.sh`.
|
||||
Go dependencies are then fetched by the go command and pip dependencies are pinned through a `requirements.txt`.
|
||||
|
||||
**We use CircleCI for continuous integration**.
|
||||
There are two workflows:
|
||||
|
||||
* `ci` runs for every commit / branch / tag pushed to GitHub.
|
||||
It is supposed to run very fast (<5min and provides quick feedback to developers).
|
||||
It runs formatting checks, lints and tests on the most important OSes / architectures.
|
||||
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
||||
|
||||
* `release` runs
|
||||
* on manual triggers through the CircleCI API (in order to produce a release)
|
||||
* periodically on `master`
|
||||
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
||||
|
||||
**Releases** are issued via Git tags + GitHub Releases feature.
|
||||
The procedure to issue a release is as follows:
|
||||
* Issue the source release:
|
||||
* Git tag the release on the `master` branch.
|
||||
* Push the tag.
|
||||
* Run `./docs/publish.sh` to re-build & push zrepl.github.io.
|
||||
* Issue the official binary release:
|
||||
* Run the `release` pipeline (triggered via CircleCI API)
|
||||
* Download the artifacts to the release manager's machine.
|
||||
* Create a GitHub release, edit the changelog, upload all the release artifacts, including .rpm and .deb files.
|
||||
* Issue the GitHub release.
|
||||
* Add the .rpm and .deb files to the official zrepl repos, publish those.
|
||||
|
||||
**Official binary releases are not re-built when Go receives an update. If the Go update is critical to zrepl (e.g. a Go security update that affects zrepl), we'd issue a new source release**.
|
||||
The rationale for this is that whereas distros provide a mechanism for this (`$zrepl_source_release-$distro_package_revision`), GitHub Releases doesn't which means we'd need to update the existing GitHub release's assets, which nobody would notice (no RSS feed updates, etc.).
|
||||
Downstream packagers can read the changelog to determine whether they want to push that minor release into their distro or simply skip it.
|
||||
|
||||
### Additional Notes to Distro Package Maintainers
|
||||
|
||||
* Use `sudo make test-platform-bin && sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||
* `dist/systemd` contains a Systemd unit template.
|
||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
|
||||
This is how `zrepl version` knows what version number to show.
|
||||
Your build system should set the `ldFlags` flags appropriately and add a prefix or suffix that indicates that the given zrepl binary is a distro build, not an official one.
|
||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||
|
||||
|
||||
## Contributing Code
|
||||
|
||||
* Open an issue when starting to hack on a new feature
|
||||
* Commits should reference the issue they are related to
|
||||
@@ -123,9 +116,6 @@ An HTML report can be generated using `make cover-html`.
|
||||
|
||||
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
|
||||
|
||||
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
||||
@@ -142,16 +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).
|
||||
|
||||
However, new contributions & patches should fix naming without further notice in the commit message.
|
||||
|
||||
### RPC debugging
|
||||
|
||||
Optionally, there are various RPC-related environment variables, that if set to something != `""` will produce additional debug output on stderr:
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/dataconn_debug.go#L11
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/stream/stream_debug.go#L11
|
||||
|
||||
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/heartbeatconn/heartbeatconn_debug.go#L11
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:latest
|
||||
FROM !SUBSTITUTED_BY_MAKEFILE
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3-pip \
|
||||
|
||||
+5
-41
@@ -3,50 +3,14 @@ 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/golangci-lint v1.35.2
|
||||
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
|
||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
||||
golang.org/x/tools v0.0.0-20210105210202-9ed45478a130
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
|
||||
google.golang.org/protobuf v1.25.0
|
||||
)
|
||||
|
||||
+530
-114
@@ -1,66 +1,133 @@
|
||||
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a h1:wFEQiK85fRsEVF0CRrPAos5LoAryUsIX1kPW/WrIqFw=
|
||||
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/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/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 h1:XTrzB+F8+SpRmbhAH8HLxhiiG6nYNwaBZjrFps1oWEk=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alvaroloes/enumer v1.1.1 h1:v+1scNqEhSFAPb9tLwblQegeTXJhnw8hf9O0amTbOvQ=
|
||||
github.com/alvaroloes/enumer v1.1.1/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/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/ashanbrown/forbidigo v1.0.0 h1:QdNXBduDUopc3GW+YVYZn8jzmIMklQiCfdN2N5+dQeE=
|
||||
github.com/ashanbrown/forbidigo v1.0.0/go.mod h1:PH+zMRWE15yW69fYfe7Kn8nYR6yYyafc3ntEGh2BBAg=
|
||||
github.com/ashanbrown/makezero v0.0.0-20201205152432-7b7cdbb3025a h1:/U9tbJzDRof4fOR51vwzWdIBsIH6R2yU0KG1MBRM2Js=
|
||||
github.com/ashanbrown/makezero v0.0.0-20201205152432-7b7cdbb3025a/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bombsimon/wsl/v3 v3.1.0 h1:E5SRssoBgtVFPcYWUOFJEcgaySgdtTNYzsSKDOY7ss8=
|
||||
github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/daixiang0/gci v0.2.8 h1:1mrIGMBQsBu0P7j7m1M8Lb+ZeZxsZL+jyGX4YoMJJpg=
|
||||
github.com/daixiang0/gci v0.2.8/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.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/denis-tingajkin/go-header v0.4.2 h1:jEeSF4sdv8/3cT/WY8AgDHUoItNSoEZ7qg9dX7pc218=
|
||||
github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-critic/go-critic v0.5.3 h1:xQEweNxzBNpSqI3wotXZAixRarETng3PTG4pkcrLCOA=
|
||||
github.com/go-critic/go-critic v0.5.3/go.mod h1:2Lrs1m4jtOnnG/EdezbSpAoL0F2pRW+9HWJUZ+QaktY=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||
github.com/go-toolsmith/astcast v1.0.0/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/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYwvlk=
|
||||
github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
|
||||
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1 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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||
@@ -69,26 +136,16 @@ github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpc
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||
github.com/golangci/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/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks=
|
||||
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||
github.com/golangci/golangci-lint v1.35.2 h1:hD1999/sq3tCPXhhI4UpunxpAAdH9pK7kDIObqoGuWA=
|
||||
github.com/golangci/golangci-lint v1.35.2/go.mod h1:Sg5fFp5oLLI1B8gXfUVUSePju8XF0uWefMkuZuGIHUo=
|
||||
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/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA=
|
||||
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||
@@ -101,182 +158,541 @@ github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0 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/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/color v1.3.1/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw=
|
||||
github.com/gostaticanalysis/analysisutil v0.4.1 h1:/7clKqrVfiVwiBQLM0Uke4KvXnO6JcCTS7HwF2D6wG8=
|
||||
github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0=
|
||||
github.com/gostaticanalysis/comment v1.3.0/go.mod h1:xMicKDx7XRXYdVwY9f9wQpDJVnqWxw9wCauCMKp+IBI=
|
||||
github.com/gostaticanalysis/comment v1.4.1 h1:xHopR5L2lRz6OsjH4R2HG5wRhW9ySl3FsHIvi5pcXwc=
|
||||
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/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/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 h1:7nkB9fLPMwtn/R6qfPcHileL/x9ydlhw8XyDrLI1ZXg=
|
||||
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
|
||||
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a h1:GmsqmapfzSJkm28dhRoHz2tLRbJmqhU86IPgBtN3mmk=
|
||||
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3 h1:jNYPNLe3d8smommaoQlK7LOA5ESyUJJ+Wf79ZtA7Vp4=
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/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/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.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/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kulti/thelper v0.2.1 h1:H4rSHiB3ALx//SXr+k9OPqKoOw2cAZpIQwVNH1RL5T4=
|
||||
github.com/kulti/thelper v0.2.1/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U=
|
||||
github.com/kunwardeep/paralleltest v1.0.2 h1:/jJRv0TiqPoEy/Y8dQxCFJhD56uS/pnvtatgTZBHokU=
|
||||
github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2GhLxSfY+oqUsvG30=
|
||||
github.com/kyoh86/exportloopref v0.1.8 h1:5Ry/at+eFdkX9Vsdw3qU4YkvGtzuVfzT4X7S77LoN/M=
|
||||
github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/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/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ=
|
||||
github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU=
|
||||
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb h1:RHba4YImhrUVQDHUCe2BNSOz4tVy2yGyXhvYDvxGgeE=
|
||||
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mbilski/exhaustivestruct v1.1.0 h1:4ykwscnAFeHJruT+EY3M3vdeP8uXMh0VV2E61iR7XD8=
|
||||
github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.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/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4=
|
||||
github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nakabonne/nestif v0.3.0 h1:+yOViDGhg8ygGrmII72nV9B/zGxY188TYpfolntsaPw=
|
||||
github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nishanths/exhaustive v0.1.0 h1:kVlMw8h2LHPMGUVqUj6230oQjjTMFjwcZrnkhXzFfl8=
|
||||
github.com/nishanths/exhaustive v0.1.0/go.mod h1:S1j9110vxV1ECdCudXRkeMnFQ/DQk9ajLT0Uf2MYZQQ=
|
||||
github.com/nishanths/predeclared v0.2.1 h1:1TXtjmy4f3YCFjTxRd8zcFHOmoUir+gp0ESzjFzG2sw=
|
||||
github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||
github.com/onsi/ginkgo v1.6.0/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/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4=
|
||||
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
|
||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d h1:CdDQnGF8Nq9ocOS/xlSptM1N3BbrA6/kmaep5ggwaIA=
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f h1:xAw10KgJqG5NJDfmRqJ05Z0IFblKumjtMeyiOLxj3+4=
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/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/quasilyte/go-ruleguard v0.2.1-0.20201030093329-408e96760278 h1:5gcJ7tORNCNB2QjOJF+MYjzS9aiWpxhP3gntf7RVrOQ=
|
||||
github.com/quasilyte/go-ruleguard v0.2.1-0.20201030093329-408e96760278/go.mod h1:2RT/tf0Ce0UDj5y243iWKosQogJd8+1G3Rs2fxmlYnw=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryancurrah/gomodguard v1.2.0 h1:YWfhGOrXwLGiqcC/u5EqG6YeS8nh+1fw0HEc85CVZro=
|
||||
github.com/ryancurrah/gomodguard v1.2.0/go.mod h1:rNqbC4TOIdUDcVMSIpNNAzTbzXAZa6W5lnUepvuMMgQ=
|
||||
github.com/ryanrolds/sqlclosecheck v0.3.0 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8OUZI9xFw=
|
||||
github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/securego/gosec/v2 v2.5.0 h1:kjfXLeKdk98gBe2+eYRFMpC4+mxmQQtbidpiiOQ69Qc=
|
||||
github.com/securego/gosec/v2 v2.5.0/go.mod h1:L/CDXVntIff5ypVHIkqPXbtRpJiNCh6c6Amn68jXDjo=
|
||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
|
||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs=
|
||||
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY=
|
||||
github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI=
|
||||
github.com/sourcegraph/go-diff v0.6.1 h1:hmA1LzxW0n1c3Q4YbrFgg4P99GSnebYa3x8gr0HZqLQ=
|
||||
github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.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/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
|
||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/ssgreg/nlreturn/v2 v2.1.0 h1:6/s4Rc49L6Uo6RLjhWZGBpWWjfzk2yrf1nIW8m4wgVA=
|
||||
github.com/ssgreg/nlreturn/v2 v2.1.0/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/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/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2 h1:Xr9gkxfOP0KQWXKNqmwe8vEeSUiUj4Rlee9CMVX2ZUQ=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
github.com/tetafro/godot v1.3.2 h1:HzWC3XjadkyeuBZxkfAFNY20UVvle0YD51I6zf6RKlU=
|
||||
github.com/tetafro/godot v1.3.2/go.mod h1:ah7jjYmOMnIjS9ku2krapvGQrFNtTLo9Z/qB3dGU1eU=
|
||||
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q=
|
||||
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20200807122107-df9e8bcb914d h1:3EZyvNUMsGD1QA8cu0STNn1L7I77rvhf2IhOcHYQhSw=
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20200807122107-df9e8bcb914d/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20201008215730-16041ac3fe65 h1:Y0bLA422kvb32uZI4fy/Plop/Tbld0l9pSzl+j1FWok=
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20201008215730-16041ac3fe65/go.mod h1:T22e7iRN4LsFPZGyRLRXeF+DWVXFuV9thsyO7NjbbTI=
|
||||
github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
|
||||
github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
|
||||
github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
|
||||
github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
|
||||
github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs=
|
||||
github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/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/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
||||
github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/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=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-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/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-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-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201011145850-ed2f50202694/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201030010431-2feb2bb1ff51/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105210202-9ed45478a130 h1:8qSBr5nyKsEgkP918Pu5FFDZpTtLIjXSo6mrtdVOFfk=
|
||||
golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 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/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.6 h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc=
|
||||
honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY=
|
||||
mvdan.cc/gofumpt v0.1.0 h1:hsVv+Y9UsZ/mFZTxJZuHVI6shSQCtzZ11h1JEFPAZLw=
|
||||
mvdan.cc/gofumpt v0.1.0/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/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=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
+3
-1
@@ -2,11 +2,13 @@
|
||||
|
||||
package main
|
||||
|
||||
// the lines are parsed by lazy.sh, do not edit
|
||||
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"
|
||||
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
||||
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
|
||||
+3
-3
@@ -11,8 +11,8 @@ import (
|
||||
)
|
||||
|
||||
var SignalCmd = &cli.Subcommand{
|
||||
Use: "signal [wakeup|reset] JOB",
|
||||
Short: "wake up a job from wait state or abort its current invocation",
|
||||
Use: "signal [replication|reset|snapshot] JOB",
|
||||
Short: "run a job replication, abort its current invocation, run a snapshot job",
|
||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||
return runSignalCmd(subcommand.Config(), args)
|
||||
},
|
||||
@@ -20,7 +20,7 @@ var SignalCmd = &cli.Subcommand{
|
||||
|
||||
func runSignalCmd(config *config.Config, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.Errorf("Expected 2 arguments: [wakeup|reset] JOB")
|
||||
return errors.Errorf("Expected 2 arguments: [replication|reset|snapshot] JOB")
|
||||
}
|
||||
|
||||
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
||||
|
||||
@@ -1,811 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"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/terminfo/s/screen" // tmux on FreeBSD 11 & 12 without ncurses
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
)
|
||||
|
||||
type byteProgressMeasurement struct {
|
||||
time time.Time
|
||||
val int64
|
||||
}
|
||||
|
||||
type bytesProgressHistory struct {
|
||||
last *byteProgressMeasurement // pointer as poor man's optional
|
||||
changeCount int
|
||||
lastChange time.Time
|
||||
bpsAvg float64
|
||||
}
|
||||
|
||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||
|
||||
if p.last == nil {
|
||||
p.last = &byteProgressMeasurement{
|
||||
time: time.Now(),
|
||||
val: currentVal,
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if p.last.val != currentVal {
|
||||
p.changeCount++
|
||||
p.lastChange = time.Now()
|
||||
}
|
||||
|
||||
if time.Since(p.lastChange) > 3*time.Second {
|
||||
p.last = nil
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
deltaV := currentVal - p.last.val
|
||||
deltaT := time.Since(p.last.time)
|
||||
rate := float64(deltaV) / deltaT.Seconds()
|
||||
|
||||
factor := 0.3
|
||||
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||
|
||||
p.last.time = time.Now()
|
||||
p.last.val = currentVal
|
||||
|
||||
return int64(p.bpsAvg), p.changeCount
|
||||
}
|
||||
|
||||
type tui struct {
|
||||
x, y int
|
||||
indent int
|
||||
|
||||
lock sync.Mutex //For report and error
|
||||
report map[string]*job.Status
|
||||
err error
|
||||
|
||||
jobFilter string
|
||||
|
||||
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||
}
|
||||
|
||||
func newTui() tui {
|
||||
return tui{
|
||||
replicationProgress: make(map[string]*bytesProgressHistory),
|
||||
}
|
||||
}
|
||||
|
||||
const INDENT_MULTIPLIER = 4
|
||||
|
||||
func (t *tui) moveLine(dl int, col int) {
|
||||
t.y += dl
|
||||
t.x = t.indent*INDENT_MULTIPLIER + col
|
||||
}
|
||||
|
||||
func (t *tui) write(text string) {
|
||||
for _, c := range text {
|
||||
if c == '\n' {
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
termbox.SetCell(t.x, t.y, c, termbox.ColorDefault, termbox.ColorDefault)
|
||||
t.x += 1
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) printf(text string, a ...interface{}) {
|
||||
t.write(fmt.Sprintf(text, a...))
|
||||
}
|
||||
|
||||
func wrap(s string, width int) string {
|
||||
var b strings.Builder
|
||||
for len(s) > 0 {
|
||||
rem := width
|
||||
if rem > len(s) {
|
||||
rem = len(s)
|
||||
}
|
||||
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
|
||||
rem = idx + 1
|
||||
}
|
||||
untilNewline := strings.TrimRight(s[:rem], "\n\r")
|
||||
s = s[rem:]
|
||||
if len(untilNewline) == 0 {
|
||||
continue
|
||||
}
|
||||
b.WriteString(untilNewline)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n\r")
|
||||
}
|
||||
|
||||
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
|
||||
whole := fmt.Sprintf(format, a...)
|
||||
width, _ := termbox.Size()
|
||||
if !strings.ContainsAny(whole, "\n\r") && t.x+len(whole) <= width {
|
||||
t.printf(format, a...)
|
||||
} else {
|
||||
t.addIndent(1)
|
||||
t.newline()
|
||||
t.write(wrap(whole, width-INDENT_MULTIPLIER*t.indent))
|
||||
t.addIndent(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) newline() {
|
||||
t.moveLine(1, 0)
|
||||
}
|
||||
|
||||
func (t *tui) setIndent(indent int) {
|
||||
t.indent = indent
|
||||
t.moveLine(0, 0)
|
||||
}
|
||||
|
||||
func (t *tui) addIndent(indent int) {
|
||||
t.indent += indent
|
||||
t.moveLine(0, 0)
|
||||
}
|
||||
|
||||
var statusFlags struct {
|
||||
Raw bool
|
||||
Job string
|
||||
}
|
||||
|
||||
var StatusCmd = &cli.Subcommand{
|
||||
Use: "status",
|
||||
Short: "show job activity or dump as JSON for monitoring",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||
},
|
||||
Run: runStatus,
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
|
||||
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if statusFlags.Raw {
|
||||
resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
||||
_, err := io.CopyN(os.Stderr, resp.Body, 4096)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.Errorf("exit")
|
||||
}
|
||||
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
t := newTui()
|
||||
t.lock.Lock()
|
||||
t.err = errors.New("Got no report yet")
|
||||
t.lock.Unlock()
|
||||
t.jobFilter = statusFlags.Job
|
||||
|
||||
err = termbox.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer termbox.Close()
|
||||
|
||||
update := func() {
|
||||
var m daemon.Status
|
||||
|
||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||
struct{}{},
|
||||
&m,
|
||||
)
|
||||
|
||||
t.lock.Lock()
|
||||
t.err = err2
|
||||
t.report = m.Jobs
|
||||
t.lock.Unlock()
|
||||
t.draw()
|
||||
}
|
||||
update()
|
||||
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
update()
|
||||
}
|
||||
}()
|
||||
|
||||
termbox.HideCursor()
|
||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||
|
||||
loop:
|
||||
for {
|
||||
switch ev := termbox.PollEvent(); ev.Type {
|
||||
case termbox.EventKey:
|
||||
switch ev.Key {
|
||||
case termbox.KeyEsc:
|
||||
break loop
|
||||
case termbox.KeyCtrlC:
|
||||
break loop
|
||||
}
|
||||
case termbox.EventResize:
|
||||
t.draw()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
|
||||
p, ok := t.replicationProgress[jobName]
|
||||
if !ok {
|
||||
p = &bytesProgressHistory{}
|
||||
t.replicationProgress[jobName] = p
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (t *tui) draw() {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||
t.x = 0
|
||||
t.y = 0
|
||||
t.indent = 0
|
||||
|
||||
if t.err != nil {
|
||||
t.write(t.err.Error())
|
||||
} else {
|
||||
//Iterate over map in alphabetical order
|
||||
keys := make([]string, 0, len(t.report))
|
||||
for k := range t.report {
|
||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||
continue
|
||||
}
|
||||
if t.jobFilter != "" && k != t.jobFilter {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
if len(keys) == 0 {
|
||||
t.setIndent(0)
|
||||
t.printf("no jobs to display")
|
||||
t.newline()
|
||||
termbox.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
v := t.report[k]
|
||||
|
||||
t.setIndent(0)
|
||||
|
||||
t.printf("Job: %s", k)
|
||||
t.setIndent(1)
|
||||
t.newline()
|
||||
t.printf("Type: %s", v.Type)
|
||||
t.setIndent(1)
|
||||
t.newline()
|
||||
|
||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
||||
if !ok || activeStatus == nil {
|
||||
t.printf("ActiveSideStatus is null")
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
|
||||
t.printf("Replication:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
|
||||
t.addIndent(-1)
|
||||
|
||||
t.printf("Pruning Sender:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(activeStatus.PruningSender)
|
||||
t.addIndent(-1)
|
||||
|
||||
t.printf("Pruning Receiver:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(activeStatus.PruningReceiver)
|
||||
t.addIndent(-1)
|
||||
|
||||
if v.Type == job.TypePush {
|
||||
t.printf("Snapshotting:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(activeStatus.Snapshotting)
|
||||
t.addIndent(-1)
|
||||
}
|
||||
|
||||
} else if v.Type == job.TypeSnap {
|
||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||
if !ok || snapStatus == nil {
|
||||
t.printf("SnapJobStatus is null")
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
t.printf("Pruning snapshots:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(snapStatus.Pruning)
|
||||
t.addIndent(-1)
|
||||
t.printf("Snapshotting:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(snapStatus.Snapshotting)
|
||||
t.addIndent(-1)
|
||||
} else if v.Type == job.TypeSource {
|
||||
|
||||
st := v.JobSpecific.(*job.PassiveStatus)
|
||||
t.printf("Snapshotting:\n")
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(st.Snapper)
|
||||
t.addIndent(-1)
|
||||
|
||||
} else {
|
||||
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||
t.newline()
|
||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||
if err != nil {
|
||||
t.printf("Error marshaling status to YAML: %s", err)
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
t.write(string(asYaml))
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
termbox.Flush()
|
||||
}
|
||||
|
||||
func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
|
||||
if rep == nil {
|
||||
t.printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
if rep.WaitReconnectError != nil {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||
t.newline()
|
||||
}
|
||||
if !rep.WaitReconnectSince.IsZero() {
|
||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||
var until string
|
||||
if rep.WaitReconnectUntil.IsZero() {
|
||||
until = "waiting indefinitely"
|
||||
} else {
|
||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
||||
}
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
||||
rep.WaitReconnectSince, until)
|
||||
} else {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
||||
if len(rep.Attempts) == 0 {
|
||||
t.printf("no attempts made yet")
|
||||
return
|
||||
} else {
|
||||
t.printf("Attempt #%d", len(rep.Attempts))
|
||||
if len(rep.Attempts) > 1 {
|
||||
t.printf(". Previous attempts failed with the following statuses:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||
t.newline()
|
||||
}
|
||||
t.addIndent(-1)
|
||||
} else {
|
||||
t.newline()
|
||||
}
|
||||
}
|
||||
|
||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
||||
})
|
||||
|
||||
t.printf("Status: %s", latest.State)
|
||||
t.newline()
|
||||
if latest.State == report.AttemptPlanningError {
|
||||
t.printf("Problem: ")
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
||||
t.newline()
|
||||
} else if latest.State == report.AttemptFanOutError {
|
||||
t.printf("Problem: one or more of the filesystems encountered errors")
|
||||
t.newline()
|
||||
}
|
||||
|
||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||
// Draw global progress bar
|
||||
// Progress: [---------------]
|
||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||
rate, changeCount := history.Update(replicated)
|
||||
eta := time.Duration(0)
|
||||
if rate > 0 {
|
||||
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||
}
|
||||
t.write("Progress: ")
|
||||
t.drawBar(50, replicated, expected, changeCount)
|
||||
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()
|
||||
if containsInvalidSizeEstimates {
|
||||
t.write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||
t.newline()
|
||||
}
|
||||
|
||||
var maxFSLen int
|
||||
for _, fs := range latest.Filesystems {
|
||||
if len(fs.Info.Name) > maxFSLen {
|
||||
maxFSLen = len(fs.Info.Name)
|
||||
}
|
||||
}
|
||||
for _, fs := range latest.Filesystems {
|
||||
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
if r == nil {
|
||||
t.printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
state, err := pruner.StateString(r.State)
|
||||
if err != nil {
|
||||
t.printf("Status: %q (parse error: %q)\n", r.State, err)
|
||||
return
|
||||
}
|
||||
|
||||
t.printf("Status: %s", state)
|
||||
t.newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.printf("Error: %s\n", r.Error)
|
||||
}
|
||||
|
||||
type commonFS struct {
|
||||
*pruner.FSReport
|
||||
completed bool
|
||||
}
|
||||
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||
for i := range r.Pending {
|
||||
all = append(all, commonFS{&r.Pending[i], false})
|
||||
}
|
||||
for i := range r.Completed {
|
||||
all = append(all, commonFS{&r.Completed[i], true})
|
||||
}
|
||||
|
||||
switch state {
|
||||
case pruner.Plan:
|
||||
fallthrough
|
||||
case pruner.PlanErr:
|
||||
return
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
t.printf("nothing to do\n")
|
||||
return
|
||||
}
|
||||
|
||||
var totalDestroyCount, completedDestroyCount int
|
||||
var maxFSname int
|
||||
for _, fs := range all {
|
||||
totalDestroyCount += len(fs.DestroyList)
|
||||
if fs.completed {
|
||||
completedDestroyCount += len(fs.DestroyList)
|
||||
}
|
||||
if maxFSname < len(fs.Filesystem) {
|
||||
maxFSname = len(fs.Filesystem)
|
||||
}
|
||||
}
|
||||
|
||||
// global progress bar
|
||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||
t.write("Progress: ")
|
||||
t.write("[")
|
||||
t.write(times("=", progress))
|
||||
t.write(">")
|
||||
t.write(times("-", 80-progress))
|
||||
t.write("]")
|
||||
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||
t.newline()
|
||||
|
||||
sort.SliceStable(all, func(i, j int) bool {
|
||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||
})
|
||||
|
||||
// Draw a table-like representation of 'all'
|
||||
for _, fs := range all {
|
||||
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
||||
t.write(" ")
|
||||
if !fs.SkipReason.NotSkipped() {
|
||||
t.printf("skipped: %s\n", fs.SkipReason)
|
||||
continue
|
||||
}
|
||||
if fs.LastError != "" {
|
||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||
t.printf("ERROR:")
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||
} else {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||
}
|
||||
t.newline()
|
||||
continue
|
||||
}
|
||||
|
||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
||||
len(fs.DestroyList), len(fs.SnapshotList))
|
||||
|
||||
if fs.completed {
|
||||
t.printf("Completed %s\n", pruneRuleActionStr)
|
||||
continue
|
||||
}
|
||||
|
||||
t.write("Pending ") // whitespace is padding 10
|
||||
if len(fs.DestroyList) == 1 {
|
||||
t.write(fs.DestroyList[0].Name)
|
||||
} else {
|
||||
t.write(pruneRuleActionStr)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
||||
if r == nil {
|
||||
t.printf("<snapshot type does not have a report>\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.printf("Status: %s", r.State)
|
||||
t.newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.printf("Error: %s\n", r.Error)
|
||||
}
|
||||
if !r.SleepUntil.IsZero() {
|
||||
t.printf("Sleep until: %s\n", r.SleepUntil)
|
||||
}
|
||||
|
||||
sort.Slice(r.Progress, func(i, j int) bool {
|
||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||
})
|
||||
|
||||
t.addIndent(1)
|
||||
defer t.addIndent(-1)
|
||||
dur := func(d time.Duration) string {
|
||||
return d.Round(100 * time.Millisecond).String()
|
||||
}
|
||||
|
||||
type row struct {
|
||||
path, state, duration, remainder, hookReport string
|
||||
}
|
||||
var widths struct {
|
||||
path, state, duration int
|
||||
}
|
||||
rows := make([]*row, len(r.Progress))
|
||||
for i, fs := range r.Progress {
|
||||
r := &row{
|
||||
path: fs.Path,
|
||||
state: fs.State.String(),
|
||||
}
|
||||
if fs.HooksHadError {
|
||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||
}
|
||||
switch fs.State {
|
||||
case snapper.SnapPending:
|
||||
r.duration = "..."
|
||||
r.remainder = ""
|
||||
case snapper.SnapStarted:
|
||||
r.duration = dur(time.Since(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
case snapper.SnapDone:
|
||||
fallthrough
|
||||
case snapper.SnapError:
|
||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
}
|
||||
rows[i] = r
|
||||
if len(r.path) > widths.path {
|
||||
widths.path = len(r.path)
|
||||
}
|
||||
if len(r.state) > widths.state {
|
||||
widths.state = len(r.state)
|
||||
}
|
||||
if len(r.duration) > widths.duration {
|
||||
widths.duration = len(r.duration)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
path := rightPad(r.path, widths.path, " ")
|
||||
state := rightPad(r.state, widths.state, " ")
|
||||
duration := rightPad(r.duration, widths.duration, " ")
|
||||
t.printf("%s %s %s", path, state, duration)
|
||||
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||
if r.hookReport != "" {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func times(str string, n int) (out string) {
|
||||
for i := 0; i < n; i++ {
|
||||
out += str
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func rightPad(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
|
||||
var arrowPositions = `>\|/`
|
||||
|
||||
// changeCount = 0 indicates stall / no progress
|
||||
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||
var completedLength int
|
||||
if totalBytes > 0 {
|
||||
completedLength = int(int64(length) * bytes / totalBytes)
|
||||
if completedLength > length {
|
||||
completedLength = length
|
||||
}
|
||||
} else if totalBytes == bytes {
|
||||
completedLength = length
|
||||
}
|
||||
|
||||
t.write("[")
|
||||
t.write(times("=", completedLength))
|
||||
t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||
t.write(times("-", length-completedLength))
|
||||
t.write("]")
|
||||
}
|
||||
|
||||
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||
|
||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||
sizeEstimationImpreciseNotice := ""
|
||||
if containsInvalidSizeEstimates {
|
||||
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||
}
|
||||
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||
}
|
||||
|
||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||
strings.ToUpper(string(rep.State)),
|
||||
rep.CurrentStep, len(rep.Steps),
|
||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||
sizeEstimationImpreciseNotice,
|
||||
)
|
||||
|
||||
activeIndicator := " "
|
||||
if active {
|
||||
activeIndicator = "*"
|
||||
}
|
||||
t.printf("%s %s %s ",
|
||||
activeIndicator,
|
||||
rightPad(rep.Info.Name, maxFS, " "),
|
||||
status)
|
||||
|
||||
next := ""
|
||||
if err := rep.Error(); err != nil {
|
||||
next = err.Err
|
||||
} else if rep.State != report.FilesystemDone {
|
||||
if nextStep := rep.NextStep(); nextStep != nil {
|
||||
if nextStep.IsIncremental() {
|
||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||
} else {
|
||||
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||
}
|
||||
attribs := []string{}
|
||||
|
||||
if nextStep.Info.Resumed {
|
||||
attribs = append(attribs, "resumed")
|
||||
}
|
||||
|
||||
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||
|
||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||
} else {
|
||||
next = "" // individual FSes may still be in planning state
|
||||
}
|
||||
|
||||
}
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
|
||||
|
||||
t.newline()
|
||||
}
|
||||
|
||||
func ByteCountBinary(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
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, " ")
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
h http.Client
|
||||
}
|
||||
|
||||
func New(network, addr string) (*Client, error) {
|
||||
httpc, err := controlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{httpc}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Status() (s daemon.Status, _ error) {
|
||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus,
|
||||
struct{}{},
|
||||
&s,
|
||||
)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func (c *Client) StatusRaw() ([]byte, error) {
|
||||
var r json.RawMessage
|
||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus, struct{}{}, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (c *Client) signal(job, sig string) error {
|
||||
return jsonRequestResponse(c.h, daemon.ControlJobEndpointSignal,
|
||||
struct {
|
||||
Name string
|
||||
Op string
|
||||
}{
|
||||
Name: job,
|
||||
Op: sig,
|
||||
},
|
||||
struct{}{},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) SignalReplication(job string) error {
|
||||
return c.signal(job, "replication")
|
||||
}
|
||||
|
||||
func (c *Client) SignalSnapshot(job string) error {
|
||||
return c.signal(job, "snapshot")
|
||||
}
|
||||
|
||||
func (c *Client) SignalReset(job string) error {
|
||||
return c.signal(job, "reset")
|
||||
}
|
||||
|
||||
func controlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client http.Client, err error) {
|
||||
return http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialfunc(ctx)
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func jsonRequestResponse(c http.Client, endpoint string, req interface{}, res interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
encodeErr := json.NewEncoder(&buf).Encode(req)
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
|
||||
resp, err := c.Post("http://unix"+endpoint, "application/json", &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var msg bytes.Buffer
|
||||
_, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
|
||||
return errors.Errorf("%s", msg.String())
|
||||
}
|
||||
|
||||
decodeError := json.NewDecoder(resp.Body).Decode(&res)
|
||||
if decodeError != nil {
|
||||
return decodeError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/client/status/client"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/util/choices"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
Status() (daemon.Status, error)
|
||||
StatusRaw() ([]byte, error)
|
||||
SignalReplication(job string) error
|
||||
SignalSnapshot(job string) error
|
||||
SignalReset(job string) error
|
||||
}
|
||||
|
||||
type statusFlags struct {
|
||||
Mode choices.Choices
|
||||
Job string
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
var statusv2Flags statusFlags
|
||||
|
||||
type statusv2Mode int
|
||||
|
||||
const (
|
||||
StatusV2ModeInteractive statusv2Mode = 1 + iota
|
||||
StatusV2ModeDump
|
||||
StatusV2ModeRaw
|
||||
StatusV2ModeLegacy
|
||||
)
|
||||
|
||||
var Subcommand = &cli.Subcommand{
|
||||
Use: "status",
|
||||
Short: "retrieve & display daemon status information",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
statusv2Flags.Mode.Init(
|
||||
"interactive", StatusV2ModeInteractive,
|
||||
"dump", StatusV2ModeDump,
|
||||
"raw", StatusV2ModeRaw,
|
||||
"legacy", StatusV2ModeLegacy,
|
||||
)
|
||||
statusv2Flags.Mode.SetTypeString("mode")
|
||||
statusv2Flags.Mode.SetDefaultValue(StatusV2ModeInteractive)
|
||||
f.Var(&statusv2Flags.Mode, "mode", statusv2Flags.Mode.Usage())
|
||||
f.StringVar(&statusv2Flags.Job, "job", "", "only show specified job (works in \"dump\" and \"interactive\" mode)")
|
||||
f.DurationVarP(&statusv2Flags.Delay, "delay", "d", 1*time.Second, "use -d 3s for 3 seconds delay (minimum delay is 1s)")
|
||||
},
|
||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||
return runStatusV2Command(ctx, subcommand.Config(), args)
|
||||
},
|
||||
}
|
||||
|
||||
func runStatusV2Command(ctx context.Context, config *config.Config, args []string) error {
|
||||
|
||||
c, err := client.New("unix", config.Global.Control.SockPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "connect to daemon socket at %q", config.Global.Control.SockPath)
|
||||
}
|
||||
|
||||
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
||||
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump {
|
||||
usemode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return errors.Errorf("error: stdout is not a tty, please use --mode %s", usemode)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case StatusV2ModeInteractive:
|
||||
return interactive(c, statusv2Flags)
|
||||
case StatusV2ModeDump:
|
||||
return dump(c, statusv2Flags.Job)
|
||||
case StatusV2ModeRaw:
|
||||
return raw(c)
|
||||
case StatusV2ModeLegacy:
|
||||
return legacy(c, statusv2Flags)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||
)
|
||||
|
||||
func dump(c Client, job string) error {
|
||||
s, err := c.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if job != "" {
|
||||
if _, ok := s.Jobs[job]; !ok {
|
||||
return errors.Errorf("job %q not found", job)
|
||||
}
|
||||
}
|
||||
|
||||
width := (1 << 31) - 1
|
||||
wrap := false
|
||||
hline := strings.Repeat("-", 80)
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
wrap = true
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get terminal dimensions")
|
||||
}
|
||||
if err := screen.Init(); err != nil {
|
||||
return errors.Wrap(err, "init screen")
|
||||
}
|
||||
width, _ = screen.Size()
|
||||
screen.Fini()
|
||||
hline = strings.Repeat("-", width)
|
||||
}
|
||||
|
||||
m := viewmodel.New()
|
||||
params := viewmodel.Params{
|
||||
Report: s.Jobs,
|
||||
ReportFetchError: nil,
|
||||
SelectedJob: nil,
|
||||
FSFilter: func(s string) bool { return true },
|
||||
DetailViewWidth: width,
|
||||
DetailViewWrap: wrap,
|
||||
ShortKeybindingOverview: "",
|
||||
}
|
||||
m.Update(params)
|
||||
for _, j := range m.Jobs() {
|
||||
if job != "" && j.Name() != job {
|
||||
continue
|
||||
}
|
||||
params.SelectedJob = j
|
||||
m.Update(params)
|
||||
fmt.Println(m.SelectedJob().FullDescription())
|
||||
if job != "" {
|
||||
return nil
|
||||
} else {
|
||||
fmt.Println(hline)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
tview "gitlab.com/tslocum/cview"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||
)
|
||||
|
||||
func interactive(c Client, flag statusFlags) error {
|
||||
|
||||
// Set this so we don't overwrite the default terminal colors
|
||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
||||
tview.Styles.BorderColor = tcell.ColorDefault
|
||||
app := tview.NewApplication()
|
||||
|
||||
jobDetailSplit := tview.NewFlex()
|
||||
jobMenu := tview.NewTreeView()
|
||||
jobMenuRoot := tview.NewTreeNode("jobs")
|
||||
jobMenuRoot.SetSelectable(true)
|
||||
jobMenu.SetRoot(jobMenuRoot)
|
||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
||||
jobMenu.SetSelectedTextColor(tcell.ColorGreen)
|
||||
jobTextDetail := tview.NewTextView()
|
||||
jobTextDetail.SetWrap(false)
|
||||
|
||||
jobMenu.SetBorder(true)
|
||||
jobTextDetail.SetBorder(true)
|
||||
|
||||
toolbarSplit := tview.NewFlex()
|
||||
toolbarSplit.SetDirection(tview.FlexRow)
|
||||
inputBarContainer := tview.NewFlex()
|
||||
fsFilterInput := tview.NewInputField()
|
||||
fsFilterInput.SetBorder(false)
|
||||
fsFilterInput.SetFieldBackgroundColor(tcell.ColorDefault)
|
||||
inputBarLabel := tview.NewTextView()
|
||||
inputBarLabel.SetText("[::b]FILTER ")
|
||||
inputBarLabel.SetDynamicColors(true)
|
||||
inputBarContainer.AddItem(inputBarLabel, 7, 1, false)
|
||||
inputBarContainer.AddItem(fsFilterInput, 0, 10, false)
|
||||
toolbarSplit.AddItem(inputBarContainer, 1, 0, false)
|
||||
toolbarSplit.AddItem(jobDetailSplit, 0, 10, false)
|
||||
|
||||
bottombar := tview.NewFlex()
|
||||
bottombar.SetDirection(tview.FlexColumn)
|
||||
bottombarDateView := tview.NewTextView()
|
||||
bottombar.AddItem(bottombarDateView, len(time.Now().String()), 0, false)
|
||||
bottomBarStatus := tview.NewTextView()
|
||||
bottomBarStatus.SetDynamicColors(true)
|
||||
bottomBarStatus.SetTextAlign(tview.AlignRight)
|
||||
bottombar.AddItem(bottomBarStatus, 0, 10, false)
|
||||
toolbarSplit.AddItem(bottombar, 1, 0, false)
|
||||
|
||||
tabbableWithJobMenu := []tview.Primitive{jobMenu, jobTextDetail, fsFilterInput}
|
||||
tabbableWithoutJobMenu := []tview.Primitive{jobTextDetail, fsFilterInput}
|
||||
var tabbable []tview.Primitive
|
||||
tabbableActiveIndex := 0
|
||||
tabbableRedraw := func() {
|
||||
if len(tabbable) == 0 {
|
||||
app.SetFocus(nil)
|
||||
return
|
||||
}
|
||||
if tabbableActiveIndex >= len(tabbable) {
|
||||
app.SetFocus(tabbable[0])
|
||||
return
|
||||
}
|
||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
||||
}
|
||||
tabbableCycle := func() {
|
||||
if len(tabbable) == 0 {
|
||||
return
|
||||
}
|
||||
tabbableActiveIndex = (tabbableActiveIndex + 1) % len(tabbable)
|
||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
||||
tabbableRedraw()
|
||||
}
|
||||
|
||||
jobMenuVisisble := false
|
||||
reconfigureJobDetailSplit := func(setJobMenuVisible bool) {
|
||||
if jobMenuVisisble == setJobMenuVisible {
|
||||
return
|
||||
}
|
||||
jobMenuVisisble = setJobMenuVisible
|
||||
if setJobMenuVisible {
|
||||
jobDetailSplit.RemoveItem(jobTextDetail)
|
||||
jobDetailSplit.AddItem(jobMenu, 0, 1, true)
|
||||
jobDetailSplit.AddItem(jobTextDetail, 0, 5, false)
|
||||
tabbable = tabbableWithJobMenu
|
||||
} else {
|
||||
jobDetailSplit.RemoveItem(jobMenu)
|
||||
tabbable = tabbableWithoutJobMenu
|
||||
}
|
||||
tabbableRedraw()
|
||||
}
|
||||
|
||||
showModal := func(m *tview.Modal, modalDoneFunc func(idx int, label string)) {
|
||||
preModalFocus := app.GetFocus()
|
||||
m.SetDoneFunc(func(idx int, label string) {
|
||||
if modalDoneFunc != nil {
|
||||
modalDoneFunc(idx, label)
|
||||
}
|
||||
app.SetRoot(toolbarSplit, true)
|
||||
app.SetFocus(preModalFocus)
|
||||
app.Draw()
|
||||
})
|
||||
app.SetRoot(m, true)
|
||||
app.Draw()
|
||||
}
|
||||
|
||||
app.SetRoot(toolbarSplit, true)
|
||||
// initial focus
|
||||
tabbableActiveIndex = len(tabbable)
|
||||
tabbableCycle()
|
||||
reconfigureJobDetailSplit(true)
|
||||
|
||||
m := viewmodel.New()
|
||||
params := &viewmodel.Params{
|
||||
Report: nil,
|
||||
SelectedJob: nil,
|
||||
FSFilter: func(_ string) bool { return true },
|
||||
DetailViewWidth: 100,
|
||||
DetailViewWrap: false,
|
||||
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
||||
}
|
||||
paramsMtx := &sync.Mutex{}
|
||||
var redraw func()
|
||||
viewmodelupdate := func(cb func(*viewmodel.Params)) {
|
||||
paramsMtx.Lock()
|
||||
defer paramsMtx.Unlock()
|
||||
cb(params)
|
||||
m.Update(*params)
|
||||
}
|
||||
redraw = func() {
|
||||
jobs := m.Jobs()
|
||||
if flag.Job != "" {
|
||||
job_found := false
|
||||
for _, job := range jobs {
|
||||
if strings.Compare(flag.Job, job.Name()) == 0 {
|
||||
jobs = []*viewmodel.Job{job}
|
||||
job_found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !job_found {
|
||||
jobs = nil
|
||||
}
|
||||
}
|
||||
redrawJobsList := false
|
||||
var selectedJobN *tview.TreeNode
|
||||
if len(jobMenuRoot.GetChildren()) == len(jobs) {
|
||||
for i, jobN := range jobMenuRoot.GetChildren() {
|
||||
if jobN.GetReference().(*viewmodel.Job) != jobs[i] {
|
||||
redrawJobsList = true
|
||||
break
|
||||
}
|
||||
if jobN.GetReference().(*viewmodel.Job) == m.SelectedJob() {
|
||||
selectedJobN = jobN
|
||||
}
|
||||
}
|
||||
} else {
|
||||
redrawJobsList = true
|
||||
}
|
||||
if redrawJobsList {
|
||||
selectedJobN = nil
|
||||
children := make([]*tview.TreeNode, len(jobs))
|
||||
for i := range jobs {
|
||||
jobN := tview.NewTreeNode(jobs[i].JobTreeTitle())
|
||||
jobN.SetReference(jobs[i])
|
||||
jobN.SetSelectable(true)
|
||||
children[i] = jobN
|
||||
jobN.SetSelectedFunc(func() {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.SelectedJob = jobN.GetReference().(*viewmodel.Job)
|
||||
})
|
||||
})
|
||||
if jobs[i] == m.SelectedJob() {
|
||||
selectedJobN = jobN
|
||||
}
|
||||
}
|
||||
jobMenuRoot.SetChildren(children)
|
||||
}
|
||||
|
||||
if selectedJobN != nil && jobMenu.GetCurrentNode() != selectedJobN {
|
||||
jobMenu.SetCurrentNode(selectedJobN)
|
||||
} else if selectedJobN == nil {
|
||||
// select something, otherwise selection breaks (likely bug in tview)
|
||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
||||
}
|
||||
|
||||
if selJ := m.SelectedJob(); selJ != nil {
|
||||
jobTextDetail.SetText(selJ.FullDescription())
|
||||
} else {
|
||||
jobTextDetail.SetText("please select a job")
|
||||
}
|
||||
|
||||
bottombardatestring := m.DateString()
|
||||
bottombarDateView.SetText(bottombardatestring)
|
||||
bottombar.ResizeItem(bottombarDateView, len(bottombardatestring), 0)
|
||||
|
||||
bottomBarStatus.SetText(m.BottomBarStatus())
|
||||
|
||||
app.Draw()
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
app.Suspend(func() {
|
||||
panic(err)
|
||||
})
|
||||
}
|
||||
}()
|
||||
for {
|
||||
st, err := c.Status()
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.Report = st.Jobs
|
||||
p.ReportFetchError = err
|
||||
})
|
||||
app.QueueUpdateDraw(redraw)
|
||||
|
||||
time.Sleep(flag.Delay)
|
||||
}
|
||||
}()
|
||||
|
||||
jobMenu.SetChangedFunc(func(jobN *tview.TreeNode) {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.SelectedJob, _ = jobN.GetReference().(*viewmodel.Job)
|
||||
})
|
||||
redraw()
|
||||
jobTextDetail.ScrollToBeginning()
|
||||
})
|
||||
jobMenu.SetSelectedFunc(func(jobN *tview.TreeNode) {
|
||||
app.SetFocus(jobTextDetail)
|
||||
})
|
||||
|
||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
_, _, p.DetailViewWidth, _ = jobTextDetail.GetInnerRect()
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
||||
if e.Key() == tcell.KeyTab {
|
||||
tabbableCycle()
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && app.GetFocus() == fsFilterInput {
|
||||
return e
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == '/' {
|
||||
if app.GetFocus() != fsFilterInput {
|
||||
app.SetFocus(fsFilterInput)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == 'M' {
|
||||
reconfigureJobDetailSplit(!jobMenuVisisble)
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == 'q' {
|
||||
app.Stop()
|
||||
}
|
||||
|
||||
if e.Key() == tcell.KeyRune && e.Rune() == 'S' {
|
||||
job, ok := jobMenu.GetCurrentNode().GetReference().(*viewmodel.Job)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
signals := []string{"replication", "snapshot", "reset"}
|
||||
clientFuncs := []func(job string) error{c.SignalReplication, c.SignalSnapshot, c.SignalReset}
|
||||
sigMod := tview.NewModal()
|
||||
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
||||
sigMod.SetBorder(true)
|
||||
sigMod.GetForm().SetButtonTextColorFocused(tcell.ColorGreen)
|
||||
sigMod.AddButtons(signals)
|
||||
sigMod.SetText(fmt.Sprintf("Send a signal to job %q", job.Name()))
|
||||
showModal(sigMod, func(idx int, _ string) {
|
||||
go func() {
|
||||
if idx == -1 {
|
||||
return
|
||||
}
|
||||
err := clientFuncs[idx](job.Name())
|
||||
if err != nil {
|
||||
app.QueueUpdate(func() {
|
||||
me := tview.NewModal()
|
||||
me.SetText(fmt.Sprintf("signal error: %s", err))
|
||||
me.AddButtons([]string{"Close"})
|
||||
showModal(me, nil)
|
||||
})
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
return e
|
||||
})
|
||||
|
||||
fsFilterInput.SetChangedFunc(func(searchterm string) {
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.FSFilter = func(fs string) bool {
|
||||
r, err := regexp.Compile(searchterm)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return r.MatchString(fs)
|
||||
}
|
||||
})
|
||||
redraw()
|
||||
jobTextDetail.ScrollToBeginning()
|
||||
})
|
||||
fsFilterInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEnter {
|
||||
app.SetFocus(jobTextDetail)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
jobTextDetail.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'w' {
|
||||
// toggle wrapping
|
||||
viewmodelupdate(func(p *viewmodel.Params) {
|
||||
p.DetailViewWrap = !p.DetailViewWrap
|
||||
})
|
||||
redraw()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
return app.Run()
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
tview "gitlab.com/tslocum/cview"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
||||
)
|
||||
|
||||
func legacy(c Client, flag statusFlags) error {
|
||||
|
||||
// Set this so we don't overwrite the default terminal colors
|
||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
||||
tview.Styles.BorderColor = tcell.ColorDefault
|
||||
app := tview.NewApplication()
|
||||
|
||||
textView := tview.NewTextView()
|
||||
textView.SetWrap(true)
|
||||
textView.SetScrollable(true) // so that it allows us to set scroll position
|
||||
textView.SetScrollBarVisibility(tview.ScrollBarNever)
|
||||
|
||||
app.SetRoot(textView, true)
|
||||
|
||||
width := (1 << 31) - 1
|
||||
wrap := false
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
wrap = true
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get terminal dimensions")
|
||||
}
|
||||
if err := screen.Init(); err != nil {
|
||||
return errors.Wrap(err, "init screen")
|
||||
}
|
||||
width, _ = screen.Size()
|
||||
screen.Fini()
|
||||
}
|
||||
|
||||
paramsMtx := &sync.Mutex{}
|
||||
params := viewmodel.Params{
|
||||
Report: nil,
|
||||
ReportFetchError: nil,
|
||||
SelectedJob: nil,
|
||||
FSFilter: func(s string) bool { return true },
|
||||
DetailViewWidth: width,
|
||||
DetailViewWrap: wrap,
|
||||
ShortKeybindingOverview: "",
|
||||
}
|
||||
|
||||
redraw := func() {
|
||||
textView.Clear()
|
||||
|
||||
paramsMtx.Lock()
|
||||
defer paramsMtx.Unlock()
|
||||
|
||||
if params.ReportFetchError != nil {
|
||||
fmt.Fprintln(textView, params.ReportFetchError.Error())
|
||||
} else if params.Report != nil {
|
||||
m := viewmodel.New()
|
||||
m.Update(params)
|
||||
for _, j := range m.Jobs() {
|
||||
if flag.Job != "" && j.Name() != flag.Job {
|
||||
continue
|
||||
}
|
||||
params.SelectedJob = j
|
||||
m.Update(params)
|
||||
fmt.Fprintln(textView, m.SelectedJob().FullDescription())
|
||||
if flag.Job != "" {
|
||||
break
|
||||
} else {
|
||||
hline := strings.Repeat("-", params.DetailViewWidth)
|
||||
fmt.Fprintln(textView, hline)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(textView, "waiting for request results")
|
||||
}
|
||||
textView.ScrollToBeginning()
|
||||
}
|
||||
|
||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
||||
// sync resizes to `params`
|
||||
paramsMtx.Lock()
|
||||
_, _, newWidth, _ := textView.GetInnerRect()
|
||||
if newWidth != params.DetailViewWidth {
|
||||
params.DetailViewWidth = newWidth
|
||||
app.QueueUpdateDraw(redraw)
|
||||
}
|
||||
paramsMtx.Unlock()
|
||||
|
||||
textView.ScrollToBeginning() // has the effect of inhibiting user scrolls
|
||||
return false
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
app.Suspend(func() {
|
||||
panic(err)
|
||||
})
|
||||
}
|
||||
}()
|
||||
for {
|
||||
st, err := c.Status()
|
||||
paramsMtx.Lock()
|
||||
params.Report = st.Jobs
|
||||
params.ReportFetchError = err
|
||||
paramsMtx.Unlock()
|
||||
|
||||
app.QueueUpdateDraw(redraw)
|
||||
|
||||
time.Sleep(flag.Delay)
|
||||
}
|
||||
}()
|
||||
|
||||
return app.Run()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func raw(c Client) error {
|
||||
b, err := c.StatusRaw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(os.Stdout, bytes.NewReader(b)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package viewmodel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ByteCountBinary(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package viewmodel
|
||||
|
||||
import "time"
|
||||
|
||||
type byteProgressMeasurement struct {
|
||||
time time.Time
|
||||
val int64
|
||||
}
|
||||
|
||||
type bytesProgressHistory struct {
|
||||
last *byteProgressMeasurement // pointer as poor man's optional
|
||||
changeCount int
|
||||
lastChange time.Time
|
||||
bpsAvg float64
|
||||
}
|
||||
|
||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||
|
||||
if p.last == nil {
|
||||
p.last = &byteProgressMeasurement{
|
||||
time: time.Now(),
|
||||
val: currentVal,
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if p.last.val != currentVal {
|
||||
p.changeCount++
|
||||
p.lastChange = time.Now()
|
||||
}
|
||||
|
||||
if time.Since(p.lastChange) > 3*time.Second {
|
||||
p.last = nil
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
deltaV := currentVal - p.last.val
|
||||
deltaT := time.Since(p.last.time)
|
||||
rate := float64(deltaV) / deltaT.Seconds()
|
||||
|
||||
factor := 0.3
|
||||
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||
|
||||
p.last.time = time.Now()
|
||||
p.last.val = currentVal
|
||||
|
||||
return int64(p.bpsAvg), p.changeCount
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
package viewmodel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
yaml "github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/client/status/viewmodel/stringbuilder"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
)
|
||||
|
||||
type M struct {
|
||||
jobs map[string]*Job
|
||||
jobsList []*Job
|
||||
selectedJob *Job
|
||||
dateString string
|
||||
bottomBarStatus string
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
// long-lived
|
||||
name string
|
||||
byteProgress *bytesProgressHistory
|
||||
|
||||
lastStatus *job.Status
|
||||
fulldescription string
|
||||
}
|
||||
|
||||
func New() *M {
|
||||
return &M{
|
||||
jobs: make(map[string]*Job),
|
||||
jobsList: make([]*Job, 0),
|
||||
selectedJob: nil,
|
||||
}
|
||||
}
|
||||
|
||||
type FilterFunc func(string) bool
|
||||
|
||||
type Params struct {
|
||||
Report map[string]*job.Status
|
||||
ReportFetchError error
|
||||
SelectedJob *Job
|
||||
FSFilter FilterFunc `validate:"required"`
|
||||
DetailViewWidth int `validate:"gte=1"`
|
||||
DetailViewWrap bool
|
||||
ShortKeybindingOverview string
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
func (m *M) Update(p Params) {
|
||||
|
||||
if err := validate.Struct(p); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if p.ReportFetchError != nil {
|
||||
m.bottomBarStatus = fmt.Sprintf("[red::]status fetch: %s", p.ReportFetchError)
|
||||
} else {
|
||||
m.bottomBarStatus = p.ShortKeybindingOverview
|
||||
for jobname, st := range p.Report {
|
||||
// TODO handle job renames & deletions
|
||||
j, ok := m.jobs[jobname]
|
||||
if !ok {
|
||||
j = &Job{
|
||||
name: jobname,
|
||||
byteProgress: &bytesProgressHistory{},
|
||||
}
|
||||
m.jobs[jobname] = j
|
||||
m.jobsList = append(m.jobsList, j)
|
||||
}
|
||||
j.lastStatus = st
|
||||
}
|
||||
}
|
||||
|
||||
// filter out internal jobs
|
||||
var jobsList []*Job
|
||||
for _, j := range m.jobsList {
|
||||
if daemon.IsInternalJobName(j.name) {
|
||||
continue
|
||||
}
|
||||
jobsList = append(jobsList, j)
|
||||
}
|
||||
m.jobsList = jobsList
|
||||
|
||||
// determinism!
|
||||
sort.Slice(m.jobsList, func(i, j int) bool {
|
||||
return strings.Compare(m.jobsList[i].name, m.jobsList[j].name) < 0
|
||||
})
|
||||
|
||||
// try to not lose the selected job
|
||||
m.selectedJob = nil
|
||||
for _, j := range m.jobsList {
|
||||
j.updateFullDescription(p)
|
||||
if j == p.SelectedJob {
|
||||
m.selectedJob = j
|
||||
}
|
||||
}
|
||||
|
||||
m.dateString = time.Now().Format(time.RFC3339)
|
||||
|
||||
}
|
||||
|
||||
func (m *M) BottomBarStatus() string { return m.bottomBarStatus }
|
||||
|
||||
func (m *M) Jobs() []*Job { return m.jobsList }
|
||||
|
||||
// may be nil
|
||||
func (m *M) SelectedJob() *Job { return m.selectedJob }
|
||||
|
||||
func (m *M) DateString() string { return m.dateString }
|
||||
|
||||
func (j *Job) updateFullDescription(p Params) {
|
||||
width := p.DetailViewWidth
|
||||
if !p.DetailViewWrap {
|
||||
width = 10000000 // FIXME
|
||||
}
|
||||
b := stringbuilder.New(stringbuilder.Config{
|
||||
IndentMultiplier: 3,
|
||||
Width: width,
|
||||
})
|
||||
drawJob(b, j.name, j.lastStatus, j.byteProgress, p.FSFilter)
|
||||
j.fulldescription = b.String()
|
||||
}
|
||||
|
||||
func (j *Job) JobTreeTitle() string {
|
||||
return j.name
|
||||
}
|
||||
|
||||
func (j *Job) FullDescription() string {
|
||||
return j.fulldescription
|
||||
}
|
||||
|
||||
func (j *Job) Name() string {
|
||||
return j.name
|
||||
}
|
||||
|
||||
func drawJob(t *stringbuilder.B, name string, v *job.Status, history *bytesProgressHistory, fsfilter FilterFunc) {
|
||||
|
||||
t.Printf("Job: %s\n", name)
|
||||
t.Printf("Type: %s\n\n", v.Type)
|
||||
|
||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
||||
if !ok || activeStatus == nil {
|
||||
t.Printf("ActiveSideStatus is null")
|
||||
t.Newline()
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Replication:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderReplicationReport(t, activeStatus.Replication, history, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
t.Printf("Pruning Sender:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderPrunerReport(t, activeStatus.PruningSender, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
t.Printf("Pruning Receiver:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderPrunerReport(t, activeStatus.PruningReceiver, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
if v.Type == job.TypePush {
|
||||
t.Printf("Snapshotting:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderSnapperReport(t, activeStatus.Snapshotting, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
}
|
||||
|
||||
} else if v.Type == job.TypeSnap {
|
||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||
if !ok || snapStatus == nil {
|
||||
t.Printf("SnapJobStatus is null")
|
||||
t.Newline()
|
||||
return
|
||||
}
|
||||
t.Printf("Pruning snapshots:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderPrunerReport(t, snapStatus.Pruning, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
t.Printf("Snapshotting:")
|
||||
t.AddIndentAndNewline(1)
|
||||
renderSnapperReport(t, snapStatus.Snapshotting, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
} else if v.Type == job.TypeSource {
|
||||
|
||||
st := v.JobSpecific.(*job.PassiveStatus)
|
||||
t.Printf("Snapshotting:\n")
|
||||
t.AddIndent(1)
|
||||
renderSnapperReport(t, st.Snapper, fsfilter)
|
||||
t.AddIndentAndNewline(-1)
|
||||
|
||||
} else {
|
||||
t.Printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||
t.Newline()
|
||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||
if err != nil {
|
||||
t.Printf("Error marshaling status to YAML: %s", err)
|
||||
t.Newline()
|
||||
return
|
||||
}
|
||||
t.Write(string(asYaml))
|
||||
t.Newline()
|
||||
}
|
||||
}
|
||||
|
||||
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, active bool, maxFS int) {
|
||||
|
||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||
sizeEstimationImpreciseNotice := ""
|
||||
if containsInvalidSizeEstimates {
|
||||
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||
}
|
||||
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||
}
|
||||
|
||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||
strings.ToUpper(string(rep.State)),
|
||||
rep.CurrentStep, len(rep.Steps),
|
||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||
sizeEstimationImpreciseNotice,
|
||||
)
|
||||
|
||||
activeIndicator := " "
|
||||
if active {
|
||||
activeIndicator = "*"
|
||||
}
|
||||
t.AddIndent(1)
|
||||
t.Printf("%s %s %s ",
|
||||
activeIndicator,
|
||||
stringbuilder.RightPad(rep.Info.Name, maxFS, " "),
|
||||
status)
|
||||
|
||||
next := ""
|
||||
if err := rep.Error(); err != nil {
|
||||
next = err.Err
|
||||
} else if rep.State != report.FilesystemDone {
|
||||
if nextStep := rep.NextStep(); nextStep != nil {
|
||||
if nextStep.IsIncremental() {
|
||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||
} else {
|
||||
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||
}
|
||||
attribs := []string{}
|
||||
|
||||
if nextStep.Info.Resumed {
|
||||
attribs = append(attribs, "resumed")
|
||||
}
|
||||
|
||||
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||
|
||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||
} else {
|
||||
next = "" // individual FSes may still be in planning state
|
||||
}
|
||||
|
||||
}
|
||||
t.Printf("%s", next)
|
||||
|
||||
t.AddIndent(-1)
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *bytesProgressHistory, fsfilter FilterFunc) {
|
||||
if rep == nil {
|
||||
t.Printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
if rep.WaitReconnectError != nil {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||
t.Newline()
|
||||
}
|
||||
if !rep.WaitReconnectSince.IsZero() {
|
||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||
var until string
|
||||
if rep.WaitReconnectUntil.IsZero() {
|
||||
until = "waiting indefinitely"
|
||||
} else {
|
||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
||||
}
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
||||
rep.WaitReconnectSince, until)
|
||||
} else {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
||||
}
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
||||
if len(rep.Attempts) == 0 {
|
||||
t.Printf("no attempts made yet")
|
||||
return
|
||||
} else {
|
||||
t.Printf("Attempt #%d", len(rep.Attempts))
|
||||
if len(rep.Attempts) > 1 {
|
||||
t.Printf(". Previous attempts failed with the following statuses:")
|
||||
t.AddIndentAndNewline(1)
|
||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)\n", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||
}
|
||||
t.AddIndentAndNewline(-1)
|
||||
} else {
|
||||
t.Newline()
|
||||
}
|
||||
}
|
||||
|
||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
||||
})
|
||||
|
||||
// apply filter
|
||||
filtered := make([]*report.FilesystemReport, 0, len(latest.Filesystems))
|
||||
for _, fs := range latest.Filesystems {
|
||||
if !fsfilter(fs.Info.Name) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, fs)
|
||||
}
|
||||
latest.Filesystems = filtered
|
||||
|
||||
t.Printf("Status: %s", latest.State)
|
||||
t.Newline()
|
||||
if !latest.FinishAt.IsZero() {
|
||||
t.Printf("Last Run: %s (lasted %s)\n", latest.FinishAt.Round(time.Second), latest.FinishAt.Sub(latest.StartAt).Round(time.Second))
|
||||
} else {
|
||||
t.Printf("Started: %s (lasting %s)\n", latest.StartAt.Round(time.Second), time.Since(latest.StartAt).Round(time.Second))
|
||||
}
|
||||
|
||||
if latest.State == report.AttemptPlanningError {
|
||||
t.Printf("Problem: ")
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
||||
t.Newline()
|
||||
} else if latest.State == report.AttemptFanOutError {
|
||||
t.Printf("Problem: one or more of the filesystems encountered errors")
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||
// Draw global progress bar
|
||||
// Progress: [---------------]
|
||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||
rate, changeCount := history.Update(replicated)
|
||||
t.Write("Progress: ")
|
||||
t.DrawBar(50, replicated, expected, changeCount)
|
||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||
t.Newline()
|
||||
if containsInvalidSizeEstimates {
|
||||
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
if len(latest.Filesystems) == 0 {
|
||||
t.Write("NOTE: no filesystems were considered for replication!")
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
var maxFSLen int
|
||||
for _, fs := range latest.Filesystems {
|
||||
if len(fs.Info.Name) > maxFSLen {
|
||||
maxFSLen = len(fs.Info.Name)
|
||||
}
|
||||
}
|
||||
for _, fs := range latest.Filesystems {
|
||||
printFilesystemStatus(t, fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
||||
if r == nil {
|
||||
t.Printf("...\n")
|
||||
return
|
||||
}
|
||||
|
||||
state, err := pruner.StateString(r.State)
|
||||
if err != nil {
|
||||
t.Printf("Status: %q (parse error: %q)\n", r.State, err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Status: %s", state)
|
||||
t.Newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.Printf("Error: %s\n", r.Error)
|
||||
}
|
||||
|
||||
type commonFS struct {
|
||||
*pruner.FSReport
|
||||
completed bool
|
||||
}
|
||||
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||
for i := range r.Pending {
|
||||
all = append(all, commonFS{&r.Pending[i], false})
|
||||
}
|
||||
for i := range r.Completed {
|
||||
all = append(all, commonFS{&r.Completed[i], true})
|
||||
}
|
||||
|
||||
// filter all
|
||||
filtered := make([]commonFS, 0, len(all))
|
||||
for _, fs := range all {
|
||||
if fsfilter(fs.FSReport.Filesystem) {
|
||||
filtered = append(filtered, fs)
|
||||
}
|
||||
}
|
||||
all = filtered
|
||||
|
||||
switch state {
|
||||
case pruner.Plan:
|
||||
fallthrough
|
||||
case pruner.PlanErr:
|
||||
return
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
t.Printf("nothing to do\n")
|
||||
return
|
||||
}
|
||||
|
||||
var totalDestroyCount, completedDestroyCount int
|
||||
var maxFSname int
|
||||
for _, fs := range all {
|
||||
totalDestroyCount += len(fs.DestroyList)
|
||||
if fs.completed {
|
||||
completedDestroyCount += len(fs.DestroyList)
|
||||
}
|
||||
if maxFSname < len(fs.Filesystem) {
|
||||
maxFSname = len(fs.Filesystem)
|
||||
}
|
||||
}
|
||||
|
||||
// global progress bar
|
||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||
t.Write("Progress: ")
|
||||
t.Write("[")
|
||||
t.Write(stringbuilder.Times("=", progress))
|
||||
t.Write(">")
|
||||
t.Write(stringbuilder.Times("-", 80-progress))
|
||||
t.Write("]")
|
||||
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||
t.Newline()
|
||||
|
||||
sort.SliceStable(all, func(i, j int) bool {
|
||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||
})
|
||||
|
||||
// Draw a table-like representation of 'all'
|
||||
for _, fs := range all {
|
||||
t.Write(stringbuilder.RightPad(fs.Filesystem, maxFSname, " "))
|
||||
t.Write(" ")
|
||||
if !fs.SkipReason.NotSkipped() {
|
||||
t.Printf("skipped: %s\n", fs.SkipReason)
|
||||
continue
|
||||
}
|
||||
if fs.LastError != "" {
|
||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||
t.Printf("ERROR:")
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||
} else {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||
}
|
||||
t.Newline()
|
||||
continue
|
||||
}
|
||||
|
||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
||||
len(fs.DestroyList), len(fs.SnapshotList))
|
||||
|
||||
if fs.completed {
|
||||
t.Printf("Completed %s\n", pruneRuleActionStr)
|
||||
continue
|
||||
}
|
||||
|
||||
t.Write("Pending ") // whitespace is padding 10
|
||||
if len(fs.DestroyList) == 1 {
|
||||
t.Write(fs.DestroyList[0].Name)
|
||||
} else {
|
||||
t.Write(pruneRuleActionStr)
|
||||
}
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
||||
if r == nil {
|
||||
t.Printf("<snapshot type does not have a report>\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Status: %s", r.State)
|
||||
t.Newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.Printf("Error: %s\n", r.Error)
|
||||
}
|
||||
if !r.SleepUntil.IsZero() {
|
||||
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
||||
}
|
||||
|
||||
sort.Slice(r.Progress, func(i, j int) bool {
|
||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||
})
|
||||
|
||||
dur := func(d time.Duration) string {
|
||||
return d.Round(100 * time.Millisecond).String()
|
||||
}
|
||||
|
||||
type row struct {
|
||||
path, state, duration, remainder, hookReport string
|
||||
}
|
||||
var widths struct {
|
||||
path, state, duration int
|
||||
}
|
||||
rows := make([]*row, 0, len(r.Progress))
|
||||
for _, fs := range r.Progress {
|
||||
if !fsfilter(fs.Path) {
|
||||
continue
|
||||
}
|
||||
r := &row{
|
||||
path: fs.Path,
|
||||
state: fs.State.String(),
|
||||
}
|
||||
if fs.HooksHadError {
|
||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||
}
|
||||
switch fs.State {
|
||||
case snapper.SnapPending:
|
||||
r.duration = "..."
|
||||
r.remainder = ""
|
||||
case snapper.SnapStarted:
|
||||
r.duration = dur(time.Since(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
case snapper.SnapDone:
|
||||
fallthrough
|
||||
case snapper.SnapError:
|
||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
}
|
||||
rows = append(rows, r)
|
||||
if len(r.path) > widths.path {
|
||||
widths.path = len(r.path)
|
||||
}
|
||||
if len(r.state) > widths.state {
|
||||
widths.state = len(r.state)
|
||||
}
|
||||
if len(r.duration) > widths.duration {
|
||||
widths.duration = len(r.duration)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
path := stringbuilder.RightPad(r.path, widths.path, " ")
|
||||
state := stringbuilder.RightPad(r.state, widths.state, " ")
|
||||
duration := stringbuilder.RightPad(r.duration, widths.duration, " ")
|
||||
t.Printf("%s %s %s", path, state, duration)
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||
if r.hookReport != "" {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||
}
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package stringbuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type B struct {
|
||||
// const
|
||||
indentMultiplier int
|
||||
|
||||
// mut
|
||||
sb *strings.Builder
|
||||
indent int
|
||||
width int
|
||||
x, y int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
IndentMultiplier int `validate:"gte=1"`
|
||||
Width int `validate:"gte=1"`
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
func New(config Config) *B {
|
||||
|
||||
if err := validate.Struct(config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &B{sb: &strings.Builder{}, width: config.Width, indentMultiplier: config.IndentMultiplier}
|
||||
}
|
||||
|
||||
func (b *B) String() string { return b.sb.String() }
|
||||
|
||||
func (w *B) Newline() {
|
||||
w.Write("\n")
|
||||
}
|
||||
|
||||
func (w *B) PrintfDrawIndentedAndWrappedIfMultiline(format string, args ...interface{}) {
|
||||
whole := fmt.Sprintf(format, args...)
|
||||
if strings.ContainsAny(whole, "\n\r") {
|
||||
w.AddIndent(1)
|
||||
defer w.AddIndent(-1)
|
||||
}
|
||||
w.Write(whole)
|
||||
}
|
||||
|
||||
func (w *B) Printf(format string, args ...interface{}) {
|
||||
whole := fmt.Sprintf(format, args...)
|
||||
w.Write(whole)
|
||||
}
|
||||
|
||||
func (t *B) AddIndent(delta int) {
|
||||
t.indent += delta * t.indentMultiplier
|
||||
}
|
||||
|
||||
func (t *B) AddIndentAndNewline(delta int) {
|
||||
t.indent += delta * t.indentMultiplier
|
||||
t.Write("\n")
|
||||
}
|
||||
|
||||
func (w *B) Write(s string) {
|
||||
for _, c := range s {
|
||||
if c == '\n' {
|
||||
fmt.Fprint(w.sb, "\n")
|
||||
w.x = 0
|
||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
||||
w.x = w.indent
|
||||
w.y++
|
||||
continue
|
||||
}
|
||||
if w.x >= w.width {
|
||||
fmt.Fprint(w.sb, "\n")
|
||||
w.x = 0
|
||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
||||
w.x = w.indent
|
||||
}
|
||||
fmt.Fprintf(w.sb, "%c", c)
|
||||
w.x++
|
||||
}
|
||||
}
|
||||
|
||||
func Times(str string, n int) (out string) {
|
||||
for i := 0; i < n; i++ {
|
||||
out += str
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RightPad(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
|
||||
// changeCount = 0 indicates stall / no progress
|
||||
func (w *B) DrawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||
const arrowPositions = `>\|/`
|
||||
var completedLength int
|
||||
if totalBytes > 0 {
|
||||
completedLength = int(int64(length) * bytes / totalBytes)
|
||||
if completedLength > length {
|
||||
completedLength = length
|
||||
}
|
||||
} else if totalBytes == bytes {
|
||||
completedLength = length
|
||||
}
|
||||
|
||||
w.Write("[")
|
||||
w.Write(Times("=", completedLength))
|
||||
w.Write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||
w.Write(Times("-", length-completedLength))
|
||||
w.Write("]")
|
||||
}
|
||||
@@ -52,9 +52,9 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||
var line chainlock.L
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
wg.Add(1)
|
||||
|
||||
// print results
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
@@ -63,7 +63,7 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||
defer line.Lock().Unlock()
|
||||
if zabsListFlags.Json {
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(abstractions); err != nil {
|
||||
if err := enc.Encode(a); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
+43
-41
@@ -54,11 +54,12 @@ func (j JobEnum) Name() string {
|
||||
}
|
||||
|
||||
type ActiveJob struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
type PassiveJob struct {
|
||||
@@ -78,33 +79,14 @@ type SnapJob struct {
|
||||
}
|
||||
|
||||
type SendOptions struct {
|
||||
StepHolds SendOptionsStepHolds `yaml:"step_holds,optional"`
|
||||
|
||||
Encrypted bool `yaml:"encrypted,optional"`
|
||||
Raw bool `yaml:"raw,optional"`
|
||||
SendProperties bool `yaml:"send_properties,optional"`
|
||||
BackupProperties bool `yaml:"backup_properties,optional"`
|
||||
LargeBlocks bool `yaml:"large_blocks,optional"`
|
||||
Compressed bool `yaml:"compressed,optional"`
|
||||
EmbeddedData bool `yaml:"embbeded_data,optional"`
|
||||
}
|
||||
|
||||
type SendOptionsStepHolds struct {
|
||||
DisableIncremental bool `yaml:"disable_incremental,optional"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||
|
||||
func (l *SendOptions) SetDefault() {
|
||||
*l = SendOptions{
|
||||
Encrypted: false,
|
||||
Raw: false,
|
||||
SendProperties: false,
|
||||
BackupProperties: false,
|
||||
LargeBlocks: false,
|
||||
Compressed: false,
|
||||
EmbeddedData: false,
|
||||
}
|
||||
Encrypted bool `yaml:"encrypted,optional,default=false"`
|
||||
Raw bool `yaml:"raw,optional,default=false"`
|
||||
SendProperties bool `yaml:"send_properties,optional,default=false"`
|
||||
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
||||
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
||||
Compressed bool `yaml:"compressed,optional,default=false"`
|
||||
EmbeddedData bool `yaml:"embbeded_data,optional,default=false"`
|
||||
Saved bool `yaml:"saved,optional,default=false"`
|
||||
}
|
||||
|
||||
type RecvOptions struct {
|
||||
@@ -116,7 +98,20 @@ type RecvOptions struct {
|
||||
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||
type Replication struct {
|
||||
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
|
||||
Concurrency *ReplicationOptionsConcurrency `yaml:"concurrency,optional,fromdefaults"`
|
||||
}
|
||||
|
||||
type ReplicationOptionsProtection struct {
|
||||
Initial string `yaml:"initial,optional,default=guarantee_resumability"`
|
||||
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
|
||||
}
|
||||
|
||||
type ReplicationOptionsConcurrency struct {
|
||||
Steps int `yaml:"steps,optional,default=1"`
|
||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
||||
}
|
||||
|
||||
func (l *RecvOptions) SetDefault() {
|
||||
*l = RecvOptions{Properties: &PropertyRecvOptions{}}
|
||||
@@ -127,14 +122,6 @@ type PropertyRecvOptions struct {
|
||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||
}
|
||||
|
||||
var _ yaml.Defaulter = (*PropertyRecvOptions)(nil)
|
||||
|
||||
func (l *PropertyRecvOptions) SetDefault() {
|
||||
//*l = PropertyRecvOptions{}
|
||||
//TODO: is below necessary?
|
||||
*l = PropertyRecvOptions{Inherit: make([]zfsprop.Property, 0), Override: make(map[zfsprop.Property]string)}
|
||||
}
|
||||
|
||||
type PushJob struct {
|
||||
ActiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
@@ -142,6 +129,9 @@ type PushJob struct {
|
||||
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 {
|
||||
ActiveJob `yaml:",inline"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
@@ -149,6 +139,10 @@ type PullJob struct {
|
||||
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 {
|
||||
Interval time.Duration
|
||||
Manual bool
|
||||
@@ -186,6 +180,10 @@ type SinkJob struct {
|
||||
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 {
|
||||
PassiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
@@ -193,6 +191,9 @@ type SourceJob struct {
|
||||
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 SnapshottingEnum struct {
|
||||
@@ -348,6 +349,7 @@ type PruneKeepNotReplicated struct {
|
||||
type PruneKeepLastN struct {
|
||||
Type string `yaml:"type"`
|
||||
Count int `yaml:"count"`
|
||||
Regex string `yaml:"regex,optional"`
|
||||
}
|
||||
|
||||
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
+17
-15
@@ -5,6 +5,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||
)
|
||||
|
||||
func TestRecvOptions(t *testing.T) {
|
||||
@@ -77,55 +80,54 @@ jobs:
|
||||
`
|
||||
|
||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||
var c *Config
|
||||
|
||||
t.Run("recv_inherit_empty", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(recv_inherit_empty))
|
||||
assert.NoError(t, err)
|
||||
c := testValidConfig(t, fill(recv_inherit_empty))
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("recv_inherit", func(t *testing.T) {
|
||||
c = testValidConfig(t, fill(recv_inherit))
|
||||
c := testValidConfig(t, fill(recv_inherit))
|
||||
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
||||
assert.NotEmpty(t, inherit)
|
||||
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
||||
})
|
||||
|
||||
t.Run("recv_override_empty", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(recv_override_empty))
|
||||
assert.NoError(t, err)
|
||||
c := testValidConfig(t, fill(recv_override_empty))
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("recv_override", func(t *testing.T) {
|
||||
c = testValidConfig(t, fill(recv_override))
|
||||
c := testValidConfig(t, fill(recv_override))
|
||||
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
||||
assert.NotEmpty(t, override)
|
||||
require.Len(t, override, 1)
|
||||
require.Equal(t, "test123", override["testprop2"])
|
||||
|
||||
})
|
||||
|
||||
t.Run("recv_override_and_inherit", func(t *testing.T) {
|
||||
c = testValidConfig(t, fill(recv_override_and_inherit))
|
||||
c := testValidConfig(t, fill(recv_override_and_inherit))
|
||||
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
||||
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
||||
assert.NotEmpty(t, inherit)
|
||||
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
||||
assert.NotEmpty(t, override)
|
||||
assert.Equal(t, "test123", override["testprop2"])
|
||||
})
|
||||
|
||||
t.Run("recv_properties_empty", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(recv_properties_empty))
|
||||
assert.NoError(t, err)
|
||||
c := testValidConfig(t, fill(recv_properties_empty))
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("recv_empty", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(recv_empty))
|
||||
assert.NoError(t, err)
|
||||
c := testValidConfig(t, fill(recv_empty))
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("send_not_specified", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(recv_not_specified))
|
||||
assert.NoError(t, err)
|
||||
c := testValidConfig(t, fill(recv_not_specified))
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
|
||||
@@ -80,15 +80,14 @@ jobs:
|
||||
`
|
||||
|
||||
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))
|
||||
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))
|
||||
c := testValidConfig(t, fill(encrypted_true))
|
||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||
assert.Equal(t, true, encrypted)
|
||||
})
|
||||
@@ -140,8 +139,7 @@ jobs:
|
||||
})
|
||||
|
||||
t.Run("send_not_specified", func(t *testing.T) {
|
||||
c, err := testConfig(t, fill(send_not_specified))
|
||||
assert.NoError(t, err)
|
||||
c := testValidConfig(t, fill(send_not_specified))
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (
|
||||
return err
|
||||
}
|
||||
|
||||
intervals, err := parseRetentionGridIntervalsString(in)
|
||||
intervals, err := ParseRetentionIntervalSpec(in)
|
||||
if err != nil {
|
||||
return 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, "|")
|
||||
intervals = make([]RetentionInterval, 0, 7*len(ges))
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
# 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`
|
||||
# `zrepl signal replication push_to_drive`
|
||||
- type: push
|
||||
name: push_to_drive
|
||||
connect:
|
||||
@@ -52,12 +52,15 @@ jobs:
|
||||
}
|
||||
send:
|
||||
encrypted: true
|
||||
# disable incremental step holds so that
|
||||
# - we can yank out the backup drive during replication
|
||||
# - thereby sacrificing resumability
|
||||
# - in exchange for the replicating snapshot not sticking around until we reconnect the backup drive
|
||||
step_holds:
|
||||
disable_incremental: 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:
|
||||
|
||||
+4
-2
@@ -143,10 +143,12 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
|
||||
var err error
|
||||
switch req.Op {
|
||||
case "wakeup":
|
||||
err = j.jobs.wakeup(req.Name)
|
||||
case "replication":
|
||||
err = j.jobs.doreplication(req.Name)
|
||||
case "reset":
|
||||
err = j.jobs.reset(req.Name)
|
||||
case "snapshot":
|
||||
err = j.jobs.dosnapshot(req.Name)
|
||||
default:
|
||||
err = fmt.Errorf("operation %q is invalid", req.Op)
|
||||
}
|
||||
|
||||
+36
-12
@@ -3,6 +3,7 @@ package daemon
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
@@ -12,14 +13,16 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"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/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/job/doreplication"
|
||||
"github.com/zrepl/zrepl/daemon/job/dosnapshot"
|
||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/version"
|
||||
@@ -37,6 +40,12 @@ func Run(ctx context.Context, conf *config.Config) error {
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// The math/rand package is used presently for generating trace IDs, we
|
||||
// seed it with the current time and pid so that the IDs are mostly
|
||||
// unique.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Seed(int64(os.Getpid()))
|
||||
|
||||
outlets, err := logging.OutletsFromConfig(*conf.Global.Logging)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot build logging from config")
|
||||
@@ -123,17 +132,19 @@ type jobs struct {
|
||||
wg sync.WaitGroup
|
||||
|
||||
// m protects all fields below it
|
||||
m sync.RWMutex
|
||||
wakeups map[string]wakeup.Func // by Job.Name
|
||||
resets map[string]reset.Func // by Job.Name
|
||||
jobs map[string]job.Job
|
||||
m sync.RWMutex
|
||||
doreplications map[string]doreplication.Func // by Job.Name
|
||||
resets map[string]reset.Func // by Job.Name
|
||||
dosnapshots map[string]dosnapshot.Func // by Job.Name
|
||||
jobs map[string]job.Job
|
||||
}
|
||||
|
||||
func newJobs() *jobs {
|
||||
return &jobs{
|
||||
wakeups: make(map[string]wakeup.Func),
|
||||
resets: make(map[string]reset.Func),
|
||||
jobs: make(map[string]job.Job),
|
||||
doreplications: make(map[string]doreplication.Func),
|
||||
resets: make(map[string]reset.Func),
|
||||
dosnapshots: make(map[string]dosnapshot.Func),
|
||||
jobs: make(map[string]job.Job),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,11 +193,11 @@ func (s *jobs) status() map[string]*job.Status {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *jobs) wakeup(job string) error {
|
||||
func (s *jobs) doreplication(job string) error {
|
||||
s.m.RLock()
|
||||
defer s.m.RUnlock()
|
||||
|
||||
wu, ok := s.wakeups[job]
|
||||
wu, ok := s.doreplications[job]
|
||||
if !ok {
|
||||
return errors.Errorf("Job %s does not exist", job)
|
||||
}
|
||||
@@ -204,6 +215,17 @@ func (s *jobs) reset(job string) error {
|
||||
return wu()
|
||||
}
|
||||
|
||||
func (s *jobs) dosnapshot(job string) error {
|
||||
s.m.RLock()
|
||||
defer s.m.RUnlock()
|
||||
|
||||
wu, ok := s.dosnapshots[job]
|
||||
if !ok {
|
||||
return errors.Errorf("Job %s does not exist", job)
|
||||
}
|
||||
return wu()
|
||||
}
|
||||
|
||||
const (
|
||||
jobNamePrometheus = "_prometheus"
|
||||
jobNameControl = "_control"
|
||||
@@ -234,10 +256,12 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
||||
|
||||
s.jobs[jobName] = j
|
||||
ctx = zfscmd.WithJobID(ctx, j.Name())
|
||||
ctx, wakeup := wakeup.Context(ctx)
|
||||
ctx, doreplication := doreplication.Context(ctx)
|
||||
ctx, resetFunc := reset.Context(ctx)
|
||||
s.wakeups[jobName] = wakeup
|
||||
ctx, dosnapshotFunc := dosnapshot.Context(ctx)
|
||||
s.doreplications[jobName] = doreplication
|
||||
s.resets[jobName] = resetFunc
|
||||
s.dosnapshots[jobName] = dosnapshotFunc
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
|
||||
+73
-50
@@ -9,12 +9,13 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/log"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/job/doreplication"
|
||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
@@ -33,11 +34,14 @@ type ActiveSide struct {
|
||||
name endpoint.JobID
|
||||
connecter transport.Connecter
|
||||
|
||||
replicationDriverConfig driver.Config
|
||||
|
||||
prunerFactory *pruner.PrunerFactory
|
||||
|
||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||
promReplicationErrors prometheus.Gauge
|
||||
|
||||
tasksMtx sync.Mutex
|
||||
tasks activeSideTasks
|
||||
@@ -85,7 +89,7 @@ type activeMode interface {
|
||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||
Type() Type
|
||||
PlannerPolicy() logic.PlannerPolicy
|
||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
||||
RunPeriodic(ctx context.Context, replicationCommon chan<- struct{})
|
||||
SnapperReport() *snapper.Report
|
||||
ResetConnectBackoff()
|
||||
}
|
||||
@@ -127,8 +131,8 @@ 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{}) {
|
||||
m.snapper.Run(ctx, wakeUpCommon)
|
||||
func (m *modePush) RunPeriodic(ctx context.Context, replicationCommon chan<- struct{}) {
|
||||
m.snapper.Run(ctx, replicationCommon)
|
||||
}
|
||||
|
||||
func (m *modePush) SnapperReport() *snapper.Report {
|
||||
@@ -145,30 +149,28 @@ func (m *modePush) ResetConnectBackoff() {
|
||||
|
||||
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||
m := &modePush{}
|
||||
var err error
|
||||
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
return nil, errors.Wrap(err, "sender config")
|
||||
}
|
||||
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
JobID: jobID,
|
||||
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
|
||||
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
SendRaw: in.Send.Raw,
|
||||
SendProperties: in.Send.SendProperties,
|
||||
SendBackupProperties: in.Send.BackupProperties,
|
||||
SendLargeBlocks: in.Send.LargeBlocks,
|
||||
SendCompressed: in.Send.Compressed,
|
||||
SendEmbeddedData: in.Send.EmbeddedData,
|
||||
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),
|
||||
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||
ReplicationConfig: replicationConfig,
|
||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||
}
|
||||
if err := m.plannerPolicy.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build planner policy")
|
||||
}
|
||||
|
||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build snapper")
|
||||
}
|
||||
|
||||
@@ -180,7 +182,6 @@ type modePull struct {
|
||||
receiver *endpoint.Receiver
|
||||
receiverConfig endpoint.ReceiverConfig
|
||||
sender *rpc.Client
|
||||
rootFS *zfs.DatasetPath
|
||||
plannerPolicy *logic.PlannerPolicy
|
||||
interval config.PositiveDurationOrManual
|
||||
}
|
||||
@@ -213,10 +214,10 @@ 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, replicationCommon chan<- struct{}) {
|
||||
if m.interval.Manual {
|
||||
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
||||
// "waiting for wakeups" is printed in common ActiveSide.do
|
||||
// "waiting for wakeup replications" is printed in common ActiveSide.do
|
||||
return
|
||||
}
|
||||
t := time.NewTicker(m.interval.Interval)
|
||||
@@ -225,12 +226,12 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
||||
select {
|
||||
case <-t.C:
|
||||
select {
|
||||
case wakeUpCommon <- struct{}{}:
|
||||
case replicationCommon <- struct{}{}:
|
||||
default:
|
||||
GetLogger(ctx).
|
||||
WithField("pull_interval", m.interval).
|
||||
Warn("pull job took longer than pull interval")
|
||||
wakeUpCommon <- struct{}{} // block anyways, to queue up the wakeup
|
||||
replicationCommon <- struct{}{} // block anyways, to queue up the wakeup replication
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -254,34 +255,38 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
||||
m = &modePull{}
|
||||
m.interval = in.Interval
|
||||
|
||||
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
||||
if err != nil {
|
||||
return nil, errors.New("RootFS is not a valid zfs filesystem path")
|
||||
}
|
||||
if m.rootFS.Length() <= 0 {
|
||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||
return nil, errors.Wrap(err, "field `replication`")
|
||||
}
|
||||
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.DontCare,
|
||||
EncryptedSend: logic.DontCare,
|
||||
ReplicationConfig: replicationConfig,
|
||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||
}
|
||||
if err := m.plannerPolicy.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build planner policy")
|
||||
}
|
||||
|
||||
m.receiverConfig = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: m.rootFS,
|
||||
AppendClientIdentity: false, // !
|
||||
UpdateLastReceivedHold: true,
|
||||
|
||||
InheritProperties: in.Recv.Properties.Inherit,
|
||||
OverrideProperties: in.Recv.Properties.Override,
|
||||
}
|
||||
if err := m.receiverConfig.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func replicationDriverConfigFromConfig(in *config.Replication) (c driver.Config, err error) {
|
||||
c = driver.Config{
|
||||
StepQueueConcurrency: in.Concurrency.Steps,
|
||||
MaxAttempts: envconst.Int("ZREPL_REPLICATION_MAX_ATTEMPTS", 3),
|
||||
ReconnectHardFailTimeout: envconst.Duration("ZREPL_REPLICATION_RECONNECT_HARD_FAIL_TIMEOUT", 10*time.Minute),
|
||||
}
|
||||
err = c.Validate()
|
||||
return c, err
|
||||
}
|
||||
|
||||
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||
|
||||
j = &ActiveSide{}
|
||||
@@ -317,6 +322,14 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||
}, []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)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build client")
|
||||
@@ -334,6 +347,11 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
j.replicationDriverConfig, err = replicationDriverConfigFromConfig(in.Replication)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build replication driver config")
|
||||
}
|
||||
|
||||
return j, nil
|
||||
}
|
||||
|
||||
@@ -341,6 +359,7 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
registerer.MustRegister(j.promRepStateSecs)
|
||||
registerer.MustRegister(j.promPruneSecs)
|
||||
registerer.MustRegister(j.promBytesReplicated)
|
||||
registerer.MustRegister(j.promReplicationErrors)
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||
@@ -375,7 +394,7 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
||||
return nil, false
|
||||
}
|
||||
return pull.rootFS.Copy(), true
|
||||
return pull.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||
}
|
||||
|
||||
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||
@@ -416,13 +435,13 @@ func (j *ActiveSide) Run(ctx context.Context) {
|
||||
invocationCount := 0
|
||||
outer:
|
||||
for {
|
||||
log.Info("wait for wakeups")
|
||||
log.Info("wait for replications")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.WithError(ctx.Err()).Info("context")
|
||||
break outer
|
||||
|
||||
case <-wakeup.Wait(ctx):
|
||||
case <-doreplication.Wait(ctx):
|
||||
j.mode.ResetConnectBackoff()
|
||||
case <-periodicDone:
|
||||
}
|
||||
@@ -466,13 +485,17 @@ func (j *ActiveSide) do(ctx context.Context) {
|
||||
*tasks = activeSideTasks{}
|
||||
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
||||
tasks.replicationReport, repWait = replication.Do(
|
||||
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||
ctx, j.replicationDriverConfig, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||
)
|
||||
tasks.state = ActiveSideReplicating
|
||||
})
|
||||
GetLogger(ctx).Info("start replication")
|
||||
repWait(true) // wait blocking
|
||||
repCancel() // always cancel to free up context resources
|
||||
|
||||
replicationReport := j.tasks.replicationReport()
|
||||
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
||||
|
||||
endSpan()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/transport"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type SendingJobConfig interface {
|
||||
GetFilesystems() config.FilesystemsFilter
|
||||
GetSendOptions() *config.SendOptions // must not be nil
|
||||
}
|
||||
|
||||
func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.SenderConfig, error) {
|
||||
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.GetFilesystems())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
sendOpts := in.GetSendOptions()
|
||||
return &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
JobID: jobID,
|
||||
|
||||
Encrypt: &nodefault.Bool{B: sendOpts.Encrypted},
|
||||
SendRaw: sendOpts.Raw,
|
||||
SendProperties: sendOpts.SendProperties,
|
||||
SendBackupProperties: sendOpts.BackupProperties,
|
||||
SendLargeBlocks: sendOpts.LargeBlocks,
|
||||
SendCompressed: sendOpts.Compressed,
|
||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
||||
SendSaved: sendOpts.Saved,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ReceivingJobConfig interface {
|
||||
GetRootFS() string
|
||||
GetAppendClientIdentity() bool
|
||||
GetRecvOptions() *config.RecvOptions
|
||||
}
|
||||
|
||||
func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoint.ReceiverConfig, err error) {
|
||||
rootFs, err := zfs.NewDatasetPath(in.GetRootFS())
|
||||
if err != nil {
|
||||
return rc, errors.New("root_fs is not a valid zfs filesystem path")
|
||||
}
|
||||
if rootFs.Length() <= 0 {
|
||||
return rc, errors.New("root_fs must not be empty") // duplicates error check of receiver
|
||||
}
|
||||
|
||||
recvOpts := in.GetRecvOptions()
|
||||
rc = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: rootFs,
|
||||
AppendClientIdentity: in.GetAppendClientIdentity(),
|
||||
|
||||
InheritProperties: recvOpts.Properties.Inherit,
|
||||
OverrideProperties: recvOpts.Properties.Override,
|
||||
}
|
||||
if err := rc.Validate(); err != nil {
|
||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
@@ -2,12 +2,16 @@ package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"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) {
|
||||
@@ -108,3 +112,145 @@ 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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestReplicationOptions(t *testing.T) {
|
||||
tmpl := `
|
||||
jobs:
|
||||
- name: foo
|
||||
type: push
|
||||
connect:
|
||||
type: local
|
||||
listener_name: foo
|
||||
client_identity: bar
|
||||
filesystems: {"<": true}
|
||||
%s
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 10
|
||||
`
|
||||
|
||||
type Test struct {
|
||||
name string
|
||||
input string
|
||||
expectOk func(t *testing.T, a *ActiveSide, m *modePush)
|
||||
expectError bool
|
||||
}
|
||||
|
||||
tests := []Test{
|
||||
{
|
||||
name: "defaults",
|
||||
input: `
|
||||
replication: {}
|
||||
`,
|
||||
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {},
|
||||
},
|
||||
{
|
||||
name: "steps_zero",
|
||||
input: `
|
||||
replication:
|
||||
concurrency:
|
||||
steps: 0
|
||||
`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "size_estimates_zero",
|
||||
input: `
|
||||
replication:
|
||||
concurrency:
|
||||
size_estimates: 0
|
||||
`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "custom_values",
|
||||
input: `
|
||||
replication:
|
||||
concurrency:
|
||||
steps: 23
|
||||
size_estimates: 42
|
||||
`,
|
||||
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {
|
||||
assert.Equal(t, 23, a.replicationDriverConfig.StepQueueConcurrency)
|
||||
assert.Equal(t, 42, m.plannerPolicy.SizeEstimationConcurrency)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative_values_forbidden",
|
||||
input: `
|
||||
replication:
|
||||
concurrency:
|
||||
steps: -23
|
||||
size_estimates: -42
|
||||
`,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||
|
||||
for _, ts := range tests {
|
||||
t.Run(ts.name, func(t *testing.T) {
|
||||
assert.True(t, (ts.expectError) != (ts.expectOk != nil))
|
||||
|
||||
cstr := fill(ts.input)
|
||||
t.Logf("testing config:\n%s", cstr)
|
||||
c, err := config.ParseConfigBytes([]byte(cstr))
|
||||
require.NoError(t, err)
|
||||
jobs, err := JobsFromConfig(c)
|
||||
if ts.expectOk != nil {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
a := jobs[0].(*ActiveSide)
|
||||
m := a.mode.(*modePush)
|
||||
ts.expectOk(t, a, m)
|
||||
} else if ts.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
t.Fatalf("test must define expectOk or expectError")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package doreplication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const contextKeyReplication contextKey = iota
|
||||
|
||||
func Wait(ctx context.Context) <-chan struct{} {
|
||||
wc, ok := ctx.Value(contextKeyReplication).(chan struct{})
|
||||
if !ok {
|
||||
wc = make(chan struct{})
|
||||
}
|
||||
return wc
|
||||
}
|
||||
|
||||
type Func func() error
|
||||
|
||||
var AlreadyReplicating = errors.New("already replicating")
|
||||
|
||||
func Context(ctx context.Context) (context.Context, Func) {
|
||||
wc := make(chan struct{})
|
||||
wuf := func() error {
|
||||
select {
|
||||
case wc <- struct{}{}:
|
||||
return nil
|
||||
default:
|
||||
return AlreadyReplicating
|
||||
}
|
||||
}
|
||||
return context.WithValue(ctx, contextKeyReplication, wc), wuf
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package wakeup
|
||||
package dosnapshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
type contextKey int
|
||||
|
||||
const contextKeyWakeup contextKey = iota
|
||||
const contextKeyDosnapshot contextKey = iota
|
||||
|
||||
func Wait(ctx context.Context) <-chan struct{} {
|
||||
wc, ok := ctx.Value(contextKeyWakeup).(chan struct{})
|
||||
wc, ok := ctx.Value(contextKeyDosnapshot).(chan struct{})
|
||||
if !ok {
|
||||
wc = make(chan struct{})
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func Wait(ctx context.Context) <-chan struct{} {
|
||||
|
||||
type Func func() error
|
||||
|
||||
var AlreadyWokenUp = errors.New("already woken up")
|
||||
var AlreadyDosnapshot = errors.New("already snapshotting")
|
||||
|
||||
func Context(ctx context.Context) (context.Context, Func) {
|
||||
wc := make(chan struct{})
|
||||
@@ -28,8 +28,8 @@ func Context(ctx context.Context) (context.Context, Func) {
|
||||
case wc <- struct{}{}:
|
||||
return nil
|
||||
default:
|
||||
return AlreadyWokenUp
|
||||
return AlreadyDosnapshot
|
||||
}
|
||||
}
|
||||
return context.WithValue(ctx, contextKeyWakeup, wc), wuf
|
||||
return context.WithValue(ctx, contextKeyDosnapshot, wc), wuf
|
||||
}
|
||||
+7
-32
@@ -6,10 +6,10 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
@@ -48,22 +48,9 @@ func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||
m = &modeSink{}
|
||||
|
||||
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||
}
|
||||
|
||||
m.receiverConfig = endpoint.ReceiverConfig{
|
||||
JobID: jobID,
|
||||
RootWithoutClientComponent: rootDataset,
|
||||
AppendClientIdentity: true, // !
|
||||
UpdateLastReceivedHold: true,
|
||||
|
||||
InheritProperties: in.Recv.Properties.Inherit,
|
||||
OverrideProperties: in.Recv.Properties.Override,
|
||||
}
|
||||
if err := m.receiverConfig.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -77,25 +64,13 @@ type modeSource struct {
|
||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||
// FIXME exact dedup of modePush
|
||||
m = &modeSource{}
|
||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
|
||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||
}
|
||||
m.senderConfig = &endpoint.SenderConfig{
|
||||
FSF: fsf,
|
||||
JobID: jobID,
|
||||
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
|
||||
|
||||
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||
SendRaw: in.Send.Raw,
|
||||
SendProperties: in.Send.SendProperties,
|
||||
SendBackupProperties: in.Send.BackupProperties,
|
||||
SendLargeBlocks: in.Send.LargeBlocks,
|
||||
SendCompressed: in.Send.Compressed,
|
||||
SendEmbeddedData: in.Send.EmbeddedData,
|
||||
return nil, errors.Wrap(err, "send options")
|
||||
}
|
||||
|
||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot build snapper")
|
||||
}
|
||||
|
||||
|
||||
+13
-7
@@ -4,14 +4,17 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/job/doreplication"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
@@ -28,7 +31,8 @@ type SnapJob struct {
|
||||
|
||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||
|
||||
pruner *pruner.Pruner
|
||||
prunerMtx sync.Mutex
|
||||
pruner *pruner.Pruner
|
||||
}
|
||||
|
||||
func (j *SnapJob) Name() string { return j.name.String() }
|
||||
@@ -76,9 +80,11 @@ type SnapJobStatus struct {
|
||||
func (j *SnapJob) Status() *Status {
|
||||
s := &SnapJobStatus{}
|
||||
t := j.Type()
|
||||
j.prunerMtx.Lock()
|
||||
if j.pruner != nil {
|
||||
s.Pruning = j.pruner.Report()
|
||||
}
|
||||
j.prunerMtx.Unlock()
|
||||
s.Snapshotting = j.snapper.Report()
|
||||
return &Status{Type: t, JobSpecific: s}
|
||||
}
|
||||
@@ -106,13 +112,13 @@ func (j *SnapJob) Run(ctx context.Context) {
|
||||
invocationCount := 0
|
||||
outer:
|
||||
for {
|
||||
log.Info("wait for wakeups")
|
||||
log.Info("wait for replications")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.WithError(ctx.Err()).Info("context")
|
||||
break outer
|
||||
|
||||
case <-wakeup.Wait(ctx):
|
||||
case <-doreplication.Wait(ctx):
|
||||
case <-periodicDone:
|
||||
}
|
||||
invocationCount++
|
||||
@@ -174,11 +180,11 @@ func (j *SnapJob) doPrune(ctx context.Context) {
|
||||
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},
|
||||
// FIXME DisableIncrementalStepHolds setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||
DisableIncrementalStepHolds: false,
|
||||
Encrypt: &nodefault.Bool{B: true},
|
||||
})
|
||||
j.prunerMtx.Lock()
|
||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||
j.prunerMtx.Unlock()
|
||||
log.Info("start pruning")
|
||||
j.pruner.Prune()
|
||||
log.Info("finished pruning")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
|
||||
@@ -93,11 +93,15 @@ 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"
|
||||
)
|
||||
|
||||
@@ -120,15 +124,19 @@ func RegisterMetrics(r prometheus.Registerer) {
|
||||
}
|
||||
|
||||
type traceNode struct {
|
||||
id string
|
||||
annotation string
|
||||
parentTask *traceNode
|
||||
// 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
|
||||
parentSpan *traceNode
|
||||
activeChildSpan *traceNode // nil if task or span doesn't have an active child span
|
||||
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
|
||||
@@ -137,6 +145,14 @@ type traceNode struct {
|
||||
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.
|
||||
@@ -196,8 +212,16 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -218,7 +242,11 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
||||
defer this.mtx.Lock().Unlock()
|
||||
|
||||
if this.activeChildTasks != 0 {
|
||||
panic(errors.Wrapf(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks", this.activeChildTasks))
|
||||
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
|
||||
@@ -229,8 +257,15 @@ func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc)
|
||||
|
||||
if this.parentTask != nil {
|
||||
this.parentTask.activeChildTasks--
|
||||
if debugEnabled {
|
||||
delete(this.parentTask.debugActiveChildTasks, this)
|
||||
}
|
||||
if this.parentTask.activeChildTasks < 0 {
|
||||
panic("impl error: parent task with negative activeChildTasks count")
|
||||
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
|
||||
@@ -279,6 +314,10 @@ func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc
|
||||
endedAt: time.Time{},
|
||||
}
|
||||
|
||||
if debugEnabled {
|
||||
this.debugCreationStack = string(runtimedebug.Stack())
|
||||
}
|
||||
|
||||
parentSpan.mtx.HoldWhile(func() {
|
||||
if parentSpan.activeChildSpan != nil {
|
||||
panic(ErrAlreadyActiveChildSpan)
|
||||
|
||||
@@ -43,15 +43,19 @@ func WithTaskAndSpan(ctx context.Context, task string, span string) (context.Con
|
||||
}
|
||||
|
||||
// create a span during which several child tasks are spawned using the `add` function
|
||||
//
|
||||
// IMPORTANT FOR USERS: Caller must ensure that the capturing behavior is correct, the Go linter doesn't catch this.
|
||||
func WithTaskGroup(ctx context.Context, taskGroup string) (_ context.Context, add func(f func(context.Context)), waitEnd DoneFunc) {
|
||||
var wg sync.WaitGroup
|
||||
ctx, endSpan := WithSpan(ctx, taskGroup)
|
||||
add = func(f func(context.Context)) {
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
ctx, endTask := WithTask(ctx, taskGroup)
|
||||
defer endTask()
|
||||
f(ctx)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ctx, endTask := WithTask(ctx, taskGroup)
|
||||
defer endTask()
|
||||
f(ctx)
|
||||
}()
|
||||
}
|
||||
waitEnd = func() {
|
||||
wg.Wait()
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package trace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -14,3 +17,46 @@ func TestGetCallerOrPanic(t *testing.T) {
|
||||
// zrepl prefix is stripped
|
||||
assert.Equal(t, "daemon/logging/trace.TestGetCallerOrPanic", ret)
|
||||
}
|
||||
|
||||
func TestWithTaskGroupRunTasksConcurrently(t *testing.T) {
|
||||
|
||||
// spawn a task group where each task waits for the other to start
|
||||
// => without concurrency, they would hang
|
||||
|
||||
rootCtx, endRoot := WithTaskFromStack(context.Background())
|
||||
defer endRoot()
|
||||
|
||||
_, add, waitEnd := WithTaskGroup(rootCtx, "test-task-group")
|
||||
|
||||
schedulerTimeout := 2 * time.Second
|
||||
timeout := time.After(schedulerTimeout)
|
||||
var hadTimeout uint32
|
||||
started0, started1 := make(chan struct{}), make(chan struct{})
|
||||
for i := 0; i < 2; i++ {
|
||||
i := i // capture by copy
|
||||
add(func(ctx context.Context) {
|
||||
switch i {
|
||||
case 0:
|
||||
close(started0)
|
||||
select {
|
||||
case <-started1:
|
||||
case <-timeout:
|
||||
atomic.AddUint32(&hadTimeout, 1)
|
||||
}
|
||||
case 1:
|
||||
close(started1)
|
||||
select {
|
||||
case <-started0:
|
||||
case <-timeout:
|
||||
atomic.AddUint32(&hadTimeout, 1)
|
||||
}
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
waitEnd()
|
||||
assert.Zero(t, hadTimeout, "either bad impl or scheduler timeout (which is %v)", schedulerTimeout)
|
||||
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package trace
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
)
|
||||
|
||||
const debugEnabled = false
|
||||
var debugEnabled = envconst.Bool("ZREPL_TRACE_DEBUG_ENABLED", false)
|
||||
|
||||
func debug(format string, args ...interface{}) {
|
||||
if !debugEnabled {
|
||||
|
||||
@@ -3,20 +3,11 @@ 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() {
|
||||
@@ -30,7 +21,7 @@ func genID() string {
|
||||
enc := base64.NewEncoder(base64.RawStdEncoding, &out)
|
||||
buf := make([]byte, genIdNumBytes)
|
||||
for i := 0; i < len(buf); {
|
||||
n, err := genIdPRNG.Read(buf[i:])
|
||||
n, err := rand.Read(buf[i:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/job/dosnapshot"
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"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"
|
||||
@@ -49,7 +50,7 @@ type args struct {
|
||||
ctx context.Context
|
||||
prefix string
|
||||
interval time.Duration
|
||||
fsf *filters.DatasetMapFilter
|
||||
fsf zfs.DatasetFilter
|
||||
snapshotsTaken chan<- struct{}
|
||||
hooks *hooks.List
|
||||
dryRun bool
|
||||
@@ -109,7 +110,7 @@ func getLogger(ctx context.Context) Logger {
|
||||
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
return nil, errors.New("prefix must not be empty")
|
||||
}
|
||||
@@ -210,6 +211,10 @@ func syncUp(a args, u updater) state {
|
||||
return u(func(s *Snapper) {
|
||||
s.state = Planning
|
||||
}).sf()
|
||||
case <-dosnapshot.Wait(a.ctx):
|
||||
return u(func(s *Snapper) {
|
||||
s.state = Planning
|
||||
}).sf()
|
||||
case <-a.ctx.Done():
|
||||
return onMainCtxDone(a.ctx, u)
|
||||
}
|
||||
@@ -378,12 +383,16 @@ func wait(a args, u updater) state {
|
||||
return u(func(snapper *Snapper) {
|
||||
snapper.state = Planning
|
||||
}).sf()
|
||||
case <-dosnapshot.Wait(a.ctx):
|
||||
return u(func(snapper *Snapper) {
|
||||
snapper.state = Planning
|
||||
}).sf()
|
||||
case <-a.ctx.Done():
|
||||
return onMainCtxDone(a.ctx, u)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// FIXME: properly abstract snapshotting:
|
||||
@@ -18,9 +18,9 @@ type PeriodicOrManual struct {
|
||||
s *Snapper
|
||||
}
|
||||
|
||||
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||
func (s *PeriodicOrManual) Run(ctx context.Context, replicationCommon chan<- struct{}) {
|
||||
if s.s != nil {
|
||||
s.s.Run(ctx, wakeUpCommon)
|
||||
s.s.Run(ctx, replicationCommon)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s *PeriodicOrManual) Report() *Report {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SnapshottingPeriodic:
|
||||
snapper, err := PeriodicFromConfig(g, fsf, v)
|
||||
|
||||
+175
-15
@@ -69,6 +69,139 @@
|
||||
"title": "Panel Title",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": true,
|
||||
"colorPostfix": false,
|
||||
"colorPrefix": false,
|
||||
"colorValue": false,
|
||||
"colors": [
|
||||
"#bf1b00",
|
||||
"#508642",
|
||||
"#bf1b00"
|
||||
],
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"description": "Number of filesystems that failed replications",
|
||||
"format": "none",
|
||||
"gauge": {
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"show": false,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 10
|
||||
},
|
||||
"id": 50,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "",
|
||||
"text": "",
|
||||
"to": ""
|
||||
}
|
||||
],
|
||||
"repeat": "zrepl_job_name",
|
||||
"repeatDirection": "h",
|
||||
"scopedVars": {
|
||||
"zrepl_job_name": {
|
||||
"selected": false,
|
||||
"text": "desktop_to_homesrv",
|
||||
"value": "desktop_to_homesrv"
|
||||
}
|
||||
},
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": true,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": true
|
||||
},
|
||||
"tableColumn": "__name__",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "zrepl_replication_filesystem_errors{job=\"$prom_job_name\",zrepl_job=\"$zrepl_job_name\"}",
|
||||
"format": "time_series",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"thresholds": "0,1",
|
||||
"title": "Failed replications $zrepl_job_name",
|
||||
"transparent": false,
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "80%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "All failed",
|
||||
"value": "-1"
|
||||
},
|
||||
{
|
||||
"op": "=",
|
||||
"text": "All OK",
|
||||
"value": "0"
|
||||
}
|
||||
],
|
||||
"valueName": "avg"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
@@ -87,7 +220,7 @@
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 10
|
||||
"y": 13
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 48,
|
||||
@@ -181,7 +314,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 10
|
||||
"y": 13
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 44,
|
||||
@@ -273,7 +406,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 14
|
||||
"y": 17
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 42,
|
||||
@@ -373,7 +506,7 @@
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 15
|
||||
"y": 18
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 22,
|
||||
@@ -465,7 +598,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 19
|
||||
"y": 22
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 33,
|
||||
@@ -573,7 +706,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 19
|
||||
"y": 22
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 23,
|
||||
@@ -665,7 +798,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
"y": 27
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 41,
|
||||
@@ -758,7 +891,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 24
|
||||
"y": 27
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 47,
|
||||
@@ -787,7 +920,7 @@
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [{
|
||||
"expr": "zrepl_endpoint_send_abstractions_cache_entry_count",
|
||||
"expr": "zrepl_endpoint_abstractions_cache_entry_count",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
@@ -796,7 +929,7 @@
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "send abstractions cache entry count (should not be zero and not grow unboundedly)",
|
||||
"title": "zfs abstractions cache entry count (should not be zero and not grow unboundedly)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
@@ -850,7 +983,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 29
|
||||
"y": 32
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 17,
|
||||
@@ -943,7 +1076,7 @@
|
||||
"h": 5,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 34
|
||||
"y": 37
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 19,
|
||||
@@ -1044,6 +1177,33 @@
|
||||
"tagsQuery": "",
|
||||
"type": "query",
|
||||
"useTags": false
|
||||
},
|
||||
{
|
||||
"allValue": null,
|
||||
"current": {
|
||||
"text": "All",
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"definition": "label_values(zrepl_replication_filesystem_errors{job=\"$prom_job_name\"}, zrepl_job)",
|
||||
"hide": 2,
|
||||
"includeAll": true,
|
||||
"label": "Zrepl Job Name",
|
||||
"multi": true,
|
||||
"name": "zrepl_job_name",
|
||||
"options": [],
|
||||
"query": "label_values(zrepl_replication_filesystem_errors{job=\"$prom_job_name\"}, zrepl_job)",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"tagValuesQuery": "",
|
||||
"tags": [],
|
||||
"tagsQuery": "",
|
||||
"type": "query",
|
||||
"useTags": false
|
||||
}]
|
||||
},
|
||||
"time": {
|
||||
@@ -1076,6 +1236,6 @@
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "zrepl 0.3",
|
||||
"uid": "etJuvBmGz",
|
||||
"version": 6
|
||||
}
|
||||
"uid": "etQuvBnGz",
|
||||
"version": 7
|
||||
}
|
||||
|
||||
+75
-16
@@ -6,6 +6,7 @@
|
||||
.. |docs| replace:: [DOCS]
|
||||
.. |feature| replace:: [FEATURE]
|
||||
.. |mig| replace:: **[MIGRATION]**
|
||||
.. |maint| replace:: [MAINT]
|
||||
|
||||
.. _changelog:
|
||||
|
||||
@@ -26,6 +27,59 @@ We use the following annotations for classifying changes:
|
||||
* |feature| Change that introduces new functionality.
|
||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||
* |docs| Change to the documentation.
|
||||
* |maint| Maintenance changes.
|
||||
|
||||
0.4.0
|
||||
-----
|
||||
|
||||
* |break| Change syntax to trigger a job replication, rename ``zrepl signal wakeup JOB`` to ``zrepl signal replication JOB``
|
||||
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
||||
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
||||
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
||||
* |feature| New ``zrepl status`` UI:
|
||||
|
||||
* Interactive job selection.
|
||||
* Interactively ``zrepl signal`` jobs.
|
||||
* Filter filesystems in the job view by name.
|
||||
* An approximation of the old UI is still included as `--mode legacy` but will be removed in a future release of zrepl.
|
||||
|
||||
* |bugfix| Actually use concurrency when listing zrepl abstractions & doing size estimation.
|
||||
These operations were accidentally made sequential in zrepl 0.3.
|
||||
* |bugfix| Job hang-up during second replication attempt.
|
||||
* |bugfix| Data races conditions in the dataconn rpc stack.
|
||||
* |maint| Update to protobuf v1.25 and grpc 1.35.
|
||||
|
||||
For users who skipped the 0.3.1 update: please make sure your pruning grid config is correct.
|
||||
The following bugfix in 0.3.1 :issue:`caused problems for some users <400>`:
|
||||
|
||||
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||
|
||||
.. NOTE::
|
||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
| You can support maintenance and feature development through one of the following services:
|
||||
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
| Note that PayPal processing fees are relatively high for small donations.
|
||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
0.3.1
|
||||
-----
|
||||
|
||||
Mostly a bugfix release for :ref:`zrepl 0.3 <release-0.3>`.
|
||||
|
||||
* |feature| pruning: add optional ``regex`` field to ``last_n`` rule
|
||||
* |docs| pruning: ``grid`` : improve documentation and add an example
|
||||
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||
This brings the implementation in line with the docs.
|
||||
* |bugfix| ``easyrsa`` script in docs
|
||||
* |bugfix| platformtest: fix skipping encryption-only tests on systems that don't support encryption
|
||||
* |bugfix| replication: report AttemptDone if no filesystems are replicated
|
||||
* |feature| status + replication: warning if replication succeeeded without any filesystem being replicated
|
||||
* |docs| update multi-job & multi-host setup section
|
||||
* RPM Packaging
|
||||
* CI infrastructure rework
|
||||
* Continuous deployment of that new `stable` branch to zrepl.github.io.
|
||||
|
||||
.. _release-0.3:
|
||||
|
||||
0.3
|
||||
---
|
||||
@@ -34,27 +88,38 @@ This is a big one! Headlining features:
|
||||
|
||||
* **Resumable Send & Recv Support**
|
||||
No knobs required, automatically used where supported.
|
||||
* **Hold-Protected Send & Recv**
|
||||
Automatic ZFS holds to ensure that we can always use resumable send&recv for a replication step.
|
||||
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
|
||||
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||
* **Receive-side hold on last received dataset**
|
||||
The counterpart to the replication cursor bookmark on the send-side.
|
||||
Ensures that incremental replication will always be possible between a sender and receiver.
|
||||
* **Replication Guarantees**
|
||||
Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
|
||||
By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
|
||||
|
||||
.. TIP::
|
||||
|
||||
We highly recommend studying the :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
||||
We highly recommend studying the updated :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
||||
|
||||
.. TIP::
|
||||
|
||||
Go 1.15 changed the default TLS validation policy to **require Subject Alternative Names (SAN) in certificates**.
|
||||
The openssl commands we provided in the quick-start guides up to and including the zrepl 0.3 docs seem not to work properly.
|
||||
If you encounter certificate validation errors regarding SAN and wish to continue to use your old certificates, start the zrepl daemon with env var ``GODEBUG=x509ignoreCN=0``.
|
||||
Alternatively, generate new certificates with SANs (see :ref:`both options int the TLS transport docs <transport-tcp+tlsclientauth-certgen>` ).
|
||||
|
||||
Quick-start guides:
|
||||
|
||||
* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl <quickstart-backup-to-external-disk>`.
|
||||
Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk.
|
||||
|
||||
Additional changelog:
|
||||
|
||||
* |break| Go 1.15 TLS changes mentioned above.
|
||||
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
|
||||
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`here<replication-cursor-and-last-received-hold>` and :ref:`here<step-holds-and-bookmarks>`).
|
||||
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details <zrepl-zfs-abstractions>`).
|
||||
Therefore you might need to adjust your job names.
|
||||
**Note that jobs** :issue:`cannot be renamed easily` **once you start using zrepl 0.3.**
|
||||
**Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
|
||||
* |break| |mig| replication cursor representation changed
|
||||
|
||||
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||
* zrepl now manages the :ref:`replication cursor bookmark <zrepl-zfs-abstractions>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
||||
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||
@@ -62,6 +127,7 @@ Additional changelog:
|
||||
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||
|
||||
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||
* |feature| :issue:`341` Prometheus metric for failing replications + corresponding Grafana panel
|
||||
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
|
||||
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
|
||||
@@ -77,13 +143,6 @@ Additional changelog:
|
||||
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
||||
|
||||
.. NOTE::
|
||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
| You can support maintenance and feature development through one of the following services:
|
||||
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||
| Note that PayPal processing fees are relatively high for small donations.
|
||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
0.2.1
|
||||
-----
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Configuration
|
||||
configuration/transports
|
||||
configuration/filter_syntax
|
||||
configuration/sendrecvoptions
|
||||
configuration/replication
|
||||
configuration/snapshotting
|
||||
configuration/prune
|
||||
configuration/logging
|
||||
|
||||
@@ -78,7 +78,7 @@ Job Type ``pull``
|
||||
``$root_fs/$source_path``
|
||||
* - ``interval``
|
||||
- | Interval at which to pull from the source job (e.g. ``10m``).
|
||||
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
|
||||
| ``manual`` disables periodic pulling, replication then only happens on :ref:`replication <cli-signal-replication>`.
|
||||
* - ``pruning``
|
||||
- |pruning-spec|
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job i
|
||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
||||
| | | (snap) | * Remote server to NAS behind NAT |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
||||
| | | with :ref:`local transport <transport-local>` | |
|
||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup to :ref:`locally attached disk <quickstart-backup-to-external-disk>` |
|
||||
| | | with :ref:`local transport <transport-local>` | * Backup FreeBSD boot pool |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||
| | (snap) | | | required |
|
||||
@@ -80,16 +80,19 @@ The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes
|
||||
.. TIP::
|
||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
||||
|
||||
.. _overview-passive-side--client-identity:
|
||||
|
||||
How the Passive Side Works
|
||||
--------------------------
|
||||
|
||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
||||
Each transport listener provides a client's identity to the passive side job.
|
||||
It uses the client identity for access control:
|
||||
When a client connects, the transport listener performS listener-specific access control (cert validation, IP ACLs, etc)
|
||||
and determines the *client identity*.
|
||||
The passive side job then uses this client identity as follows:
|
||||
|
||||
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||
* The ``source`` job has a whitelist of client identities that are allowed pull access.
|
||||
* The ``source`` might, in the future, embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>` in order to support multi-host replication.
|
||||
|
||||
.. TIP::
|
||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
||||
@@ -132,23 +135,10 @@ The following high-level steps take place during replication and can be monitore
|
||||
|
||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||
|
||||
Placeholder Filesystems
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
ZFS Background Knowledge
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This section gives some background knowledge about ZFS features that zrepl uses to guarantee that
|
||||
**incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
||||
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
|
||||
Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
||||
|
||||
**ZFS Send Modes & Bookmarks**
|
||||
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
|
||||
@@ -158,6 +148,22 @@ Incremental sends require that ``@from`` be present on the receiving side when r
|
||||
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
|
||||
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
|
||||
|
||||
.. _zfs-background-knowledge-plain-vs-raw-sends:
|
||||
|
||||
**Plain and raw sends**
|
||||
By default, ``zfs send`` sends the most generic, backwards-compatible data stream format (so-called 'plain send').
|
||||
If the sent uses newer features, e.g. compression or encryption, ``zfs send`` has to un-do these operations on the fly to produce the plain send stream.
|
||||
If the receiver uses newer features (e.g. compression or encryption inherited from the parent FS), it applies the necessary transformations again on the fly during ``zfs recv``.
|
||||
|
||||
Flags such as ``-e``, ``-c`` and ``-L`` tell ZFS to produce a send stream that is closer to how the data is stored on disk.
|
||||
Sending with those flags removes computational overhead from sender and receiver.
|
||||
However, the receiver will not apply certain transformations, e.g., it will not compress with the receive-side ``compression`` algorithm.
|
||||
|
||||
The ``-w`` (``--raw``) flag produces a send stream that is as *raw* as possible.
|
||||
For unencrypted datasets, its current effect is the same as ``-Lce``.
|
||||
|
||||
Encrypted datasets can only be sent plain (unencrypted) or raw (encrypted) using the ``-w`` flag.
|
||||
|
||||
**Resumable Send & Recv**
|
||||
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
|
||||
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
|
||||
@@ -166,41 +172,56 @@ An incremental send can only be resumed if ``@to`` still exists *and* either ``@
|
||||
|
||||
**ZFS Holds**
|
||||
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
|
||||
Holds are created and referred to by a user-defined *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
||||
Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
||||
|
||||
|
||||
.. _zrepl-zfs-abstractions:
|
||||
|
||||
ZFS Abstractions Managed By zrepl
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality.
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the ZFS property ``zrepl:placeholder=on``.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
.. _replication-cursor-and-last-received-hold:
|
||||
|
||||
Guaranteeing That Incremental Sends Are Always Possible
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
||||
The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
||||
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||
Both are moved aomically after the receiving side has confirmed that a replication step is complete.
|
||||
Both are moved atomically after the receiving side has confirmed that a replication step is complete.
|
||||
|
||||
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
|
||||
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||
|
||||
.. _tentative-replication-cursor-bookmarks:
|
||||
|
||||
**Tentative replication cursor bookmarks** are short-lived bookmarks that protect the atomic moving-forward of the replication cursor and last-received-hold (see :issue:`this issue <340>`).
|
||||
They are only necessary if step holds are not used as per the :ref:`replication.protection <replication-option-protection>` setting.
|
||||
The tentative replication cursor has the format ``#zrepl_CUSORTENTATIVE_G_<GUID>_J_<JOBNAME>``.
|
||||
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||
|
||||
.. _step-holds-and-bookmarks:
|
||||
|
||||
Guaranteeing That Sends Are Always Resumable
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. _step-holds:
|
||||
|
||||
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
|
||||
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
|
||||
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
|
||||
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
|
||||
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
|
||||
|
||||
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
|
||||
A job only ever has one active send per filesystem.
|
||||
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
|
||||
|
||||
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
|
||||
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` that is not managed by zrepl.
|
||||
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl.
|
||||
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
|
||||
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
|
||||
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
|
||||
@@ -227,39 +248,53 @@ Limitations
|
||||
Multiple Jobs & More than 2 Machines
|
||||
------------------------------------
|
||||
|
||||
The quick-start guides focus on simple setups with a single sender and a single receiver.
|
||||
This section documents considerations for more complex setups.
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
||||
Before you continue, make sure you have a working understanding of :ref:`how zrepl works <overview-how-replication-works>`
|
||||
and :ref:`what zrepl does to ensure <zrepl-zfs-abstractions>` that replication between sender and receiver is always
|
||||
possible without conflicts.
|
||||
This will help you understand why certain kinds of multi-machine setups do not (yet) work.
|
||||
|
||||
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
||||
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
||||
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
||||
.. NOTE::
|
||||
|
||||
Explanations & exceptions to above rules are detailed below.
|
||||
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
|
||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
Multiple Jobs on one Machine
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
|
||||
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
||||
|
||||
No Overlapping
|
||||
^^^^^^^^^^^^^^
|
||||
On your setup, ensure that
|
||||
|
||||
Jobs run independently of each other.
|
||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B assumed the snapshot to still be present.
|
||||
However, the next replication attempt will re-examine the situation from scratch and should work.
|
||||
* all ``filesystems`` filter specifications are disjoint
|
||||
* no ``root_fs`` is a prefix or equal to another ``root_fs``
|
||||
* no ``filesystems`` filter matches any ``root_fs``
|
||||
|
||||
N push jobs to 1 sink
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
**Exceptions to the rule**:
|
||||
|
||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||
It is thus safe to push to one sink job with different client identities.
|
||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||
* A ``snap`` and ``push`` job on the same machine can match the same ``filesystems``.
|
||||
To avoid interference, only one of the jobs should be pruning snapshots on the sender, the other one should keep all snapshots.
|
||||
Since the jobs won't coordinate, errors in the log are to be expected, but :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` ensure that ``push`` and ``sink`` can always replicate incrementally.
|
||||
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
||||
|
||||
N pull jobs from 1 source
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
More Than 2 Machines
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||
Thus, it is not possible to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
|
||||
|
||||
**Working setups**:
|
||||
|
||||
* N ``push`` identities, 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
|
||||
|
||||
* ``sink`` constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
|
||||
Therefore, the different clients cannot interfere.
|
||||
|
||||
|
||||
**Setups that do not work**:
|
||||
|
||||
* N ``pull`` identities, 1 ``source`` job. Tracking :issue:`380`.
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ Example Configuration:
|
||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||
regex: "^zrepl_.*"
|
||||
# manually created snapshots will be kept forever on receiver
|
||||
- type: regex
|
||||
regex: "^manual_.*"
|
||||
|
||||
.. DANGER::
|
||||
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
||||
@@ -82,34 +84,82 @@ Policy ``grid``
|
||||
- type: grid
|
||||
regex: "^zrepl_.*"
|
||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||
│ │
|
||||
└─ one hour interval
|
||||
│
|
||||
└─ 24 adjacent one-hour intervals
|
||||
│ │ │
|
||||
└─ 1 repetition of a one-hour interval with keep=all
|
||||
│ │
|
||||
└─ 24 repetitions of a one-hour interval with keep=1
|
||||
│
|
||||
└─ 6 repetitions of a 30-day interval with keep=1
|
||||
...
|
||||
|
||||
The retention grid can be thought of as a time-based sieve:
|
||||
The ``grid`` field specifies a list of adjacent time intervals:
|
||||
the left edge of the leftmost (first) interval is the ``creation`` date of the youngest snapshot.
|
||||
All intervals to its right describe time intervals further in the past.
|
||||
|
||||
Each interval carries a maximum number of snapshots to keep.
|
||||
It is specified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
|
||||
The default value is **keep=1**.
|
||||
The retention grid can be thought of as a time-based sieve that thins out snapshots as they get older.
|
||||
|
||||
The ``grid`` field specifies a list of adjacent time intervals.
|
||||
Each interval is a bucket with a maximum capacity of ``keep`` snapshots.
|
||||
The following procedure happens during pruning:
|
||||
|
||||
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
||||
Only snapshots names that match the regex are considered for this rule, all others are not affected.
|
||||
#. The filtered list of snapshots is sorted by ``creation``
|
||||
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot
|
||||
#. A list of buckets is created, one for each interval
|
||||
#. The list of snapshots is split up into the buckets.
|
||||
#. For each bucket
|
||||
Only snapshots names that match the regex are considered for this rule, all others will be pruned unless another rule keeps them.
|
||||
#. The snapshots that match ``regex`` are placed onto a time axis according to their ``creation`` date.
|
||||
The youngest snapshot is on the left, the oldest on the right.
|
||||
#. The first buckets are placed "under" that axis so that the ``grid`` spec's first bucket's left edge aligns with youngest snapshot.
|
||||
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
||||
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
||||
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
||||
#. Snapshots older than the rightmost bucket **not kept** by this gridspec.
|
||||
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
||||
|
||||
#. the contained snapshot list is sorted by creation.
|
||||
#. snapshots from the list, oldest first, are destroyed until the specified ``keep`` count is reached.
|
||||
#. all remaining snapshots on the list are kept.
|
||||
The syntax to describe the bucket list is as follows:
|
||||
|
||||
::
|
||||
|
||||
Repeat x Duration (keep=all)
|
||||
|
||||
* The **duration** specifies the length of the interval.
|
||||
* The **keep** count specifies the number of snapshots that fit into the bucket.
|
||||
It can be either a positive integer or ``all`` (all snapshots are kept).
|
||||
* The **repeat** count repeats the bucket definition for the specified number of times.
|
||||
|
||||
**Example**:
|
||||
|
||||
::
|
||||
|
||||
This grid spec produces the following list of adjacent buckets. For the sake of simplicity,
|
||||
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
|
||||
|
||||
`
|
||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||
regex: .*
|
||||
`
|
||||
|
||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||
| | | | | | | | | |
|
||||
|-Bucket1-|-----Bucket 2------|------Bucket 3-----|-----------Bucket 4----------|
|
||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||
|
||||
|
||||
|
||||
Let us consider the following set of snapshots @a-zA-C:
|
||||
|
||||
|
||||
| a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D |
|
||||
|
||||
The `grid` algorithm maps them to their respective buckets:
|
||||
|
||||
Bucket 1: a, b, c
|
||||
Bucket 2: d,e,f,g,h,i,j
|
||||
Bucket 3: k,l,m,n,o,p
|
||||
Bucket 4: q,r, q,r,s,t,u,v,w,x,y,z
|
||||
None: A,B,C,D
|
||||
|
||||
It then applies the per-bucket pruning logic described above which resulting in the
|
||||
following list of remaining snapshots.
|
||||
|
||||
| a b c j p z |
|
||||
|
||||
Note that it only makes sense to grow (not shorten) the interval duration for buckets
|
||||
further in the past since each bucket acts like a low-pass filter for incoming snapshots
|
||||
and adding a less-low-pass-filter after a low-pass one has no effect.
|
||||
|
||||
|
||||
.. _prune-keep-last-n:
|
||||
@@ -125,9 +175,11 @@ Policy ``last_n``
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 10
|
||||
regex: ^zrepl_.*$ # optional
|
||||
...
|
||||
|
||||
``last_n`` keeps the last ``count`` snapshots (last = youngest = most recent creation date).
|
||||
``last_n`` filters the snapshot list by ``regex``, then keeps the last ``count`` snapshots in that list (last = youngest = most recent creation date)
|
||||
All snapshots that don't match ``regex`` or exceed ``count`` in the filtered list are destroyed unless matched by other rules.
|
||||
|
||||
.. _prune-keep-regex:
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
|
||||
Replication Options
|
||||
===================
|
||||
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: ...
|
||||
replication:
|
||||
protection:
|
||||
initial: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||
incremental: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||
concurrency:
|
||||
size_estimates: 4
|
||||
steps: 1
|
||||
|
||||
...
|
||||
|
||||
.. _replication-option-protection:
|
||||
|
||||
``protection`` option
|
||||
--------------------------
|
||||
|
||||
The ``protection`` variable controls the degree to which a replicated filesystem is protected from getting out of sync through a zrepl pruner or external tools that destroy snapshots.
|
||||
zrepl can guarantee :ref:`resumability <step-holds>` or just :ref:`incremental replication <replication-cursor-and-last-received-hold>`.
|
||||
|
||||
``guarantee_resumability`` is the **default** value and guarantees that a replication step is always resumable and that incremental replication will always be possible.
|
||||
The implementation uses replication cursors, last-received-hold and step holds.
|
||||
|
||||
``guarantee_incremental`` only guarantees that incremental replication will always be possible.
|
||||
If a step ``from -> to`` is interrupted and its `to` snapshot is destroyed, zrepl will remove the half-received ``to``'s resume state and start a new step ``from -> to2``.
|
||||
The implementation uses replication cursors, tentative replication cursors and last-received-hold.
|
||||
|
||||
``guarantee_nothing`` does not make any guarantees with regards to keeping sending and receiving side in sync.
|
||||
No bookmarks or holds are created to protect sender and receiver from diverging.
|
||||
|
||||
**Tradeoffs**
|
||||
|
||||
Using ``guarantee_incremental`` instead of ``guarantee_resumability`` obviously removes the resumability guarantee.
|
||||
This means that replication progress is no longer monotonic which might lead to a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
|
||||
However, the advantage and :issue:`reason for existence <288>` of the ``incremental`` mode is that it allows the pruner to delete snapshots of interrupted replication steps
|
||||
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
When changing this flag, obsoleted zrepl-managed bookmarks and holds will be destroyed on the next replication step that is attempted for each filesystem.
|
||||
|
||||
|
||||
.. _replication-option-concurrency:
|
||||
|
||||
``concurrency`` option
|
||||
----------------------
|
||||
|
||||
The ``concurrency`` options control the maximum amount of concurrency during replication.
|
||||
The default values allow some concurrency during size estimation but no parallelism for the actual replication.
|
||||
|
||||
* ``concurrency.steps`` (default = 1) controls the maximum number of concurrently executed :ref:`replication steps <overview-how-replication-works>`.
|
||||
The planning step for each file system is counted as a single step.
|
||||
* ``concurrency.size_estimates`` (default = 4) controls the maximum number of concurrent step size estimations done by the job.
|
||||
|
||||
Note that initial replication cannot start replicating child filesystems before the parent filesystem's initial replication step has completed.
|
||||
|
||||
Some notes on tuning these values:
|
||||
|
||||
* Disk: Size estimation is less I/O intensive than step execution because it does not need to access the data blocks.
|
||||
* CPU: Size estimation is usually a dense CPU burst whereas step execution CPU utilization is stretched out over time because of disk IO.
|
||||
Faster disks, sending a compressed dataset in :ref:`plain mode <zfs-background-knowledge-plain-vs-raw-sends>` and the zrepl transport mode all contribute to higher CPU requirements.
|
||||
* Network bandwidth: Size estimation does not consume meaningful amounts of bandwidth, step execution does.
|
||||
* :ref:`zrepl ZFS abstractions <zrepl-zfs-abstractions>`: for each replication step zrepl needs to update its ZFS abstractions through the ``zfs`` command which often waits multiple seconds for the zpool to sync.
|
||||
Thus, if the actual send & recv time of a step is small compared to the time spent on zrepl ZFS abstractions then increasing step execution concurrency will result in a lower overall turnaround time.
|
||||
@@ -9,24 +9,62 @@ Send & Recv Options
|
||||
Send Options
|
||||
~~~~~~~~~~~~
|
||||
|
||||
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: ...
|
||||
send:
|
||||
encrypted: true
|
||||
step_holds:
|
||||
disable_incremental: false
|
||||
# flags from the table below go here
|
||||
...
|
||||
|
||||
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
||||
The following table specifies the list of (boolean) options.
|
||||
Flags with an entry in the ``zfs send`` column map directly to the zfs send CLI flags.
|
||||
zrepl does not perform feature checks for these flags.
|
||||
If you enable a flag that is not supported by the installed version of ZFS, the zfs error will show up at runtime in the logs and zrepl status.
|
||||
See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-send.8.html>`_ (``man zfs-send``) for their semantics.
|
||||
|
||||
``encryption`` option
|
||||
---------------------
|
||||
.. list-table::
|
||||
:widths: 20 10 70
|
||||
:header-rows: 1
|
||||
|
||||
The ``encryption`` variable controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
||||
More specifically, if ``encryption=true``, zrepl
|
||||
* - ``send.``
|
||||
- ``zfs send``
|
||||
- Comment
|
||||
* - ``encrypted``
|
||||
-
|
||||
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
||||
* - ``raw``
|
||||
- ``-w``
|
||||
- Use ``encrypted`` to only allow encrypted sends.
|
||||
* - ``send_properties``
|
||||
- ``-p``
|
||||
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||
* - ``backup_properties``
|
||||
- ``-b``
|
||||
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||
* - ``large_blocks``
|
||||
- ``-L``
|
||||
- **Potential data loss on OpenZFS < 2.0**, see :ref:`warning below <job-send-options-large-blocks>`.
|
||||
* - ``compressed``
|
||||
- ``-c``
|
||||
-
|
||||
* - ``embbeded_data``
|
||||
- ``-e``
|
||||
-
|
||||
* - ``saved``
|
||||
- ``-S``
|
||||
-
|
||||
|
||||
.. _job-send-options-encrypted:
|
||||
|
||||
``encrypted``
|
||||
-------------
|
||||
|
||||
The ``encrypted`` option controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
||||
More specifically, if ``encrypted=true``, zrepl
|
||||
|
||||
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
|
||||
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
||||
@@ -34,32 +72,143 @@ More specifically, if ``encryption=true``, zrepl
|
||||
|
||||
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
|
||||
|
||||
If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
||||
|
||||
.. _job-send-option-step-holds-disable-incremental:
|
||||
|
||||
``step_holds.disable_incremental`` option
|
||||
-----------------------------------------
|
||||
|
||||
The ``step_holds.disable_incremental`` variable controls whether the creation of :ref:`step holds <step-holds-and-bookmarks>` should be disabled for incremental replication.
|
||||
The default value is ``false``.
|
||||
|
||||
Disabling step holds has the disadvantage that steps :ref:`might not be resumable <step-holds-and-bookmarks>` if interrupted.
|
||||
Non-resumability means that replication progress is no longer monotonic which might result in a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
|
||||
|
||||
However, the advantage and :issue:`reason for existence <288>` of this flag is that it allows the pruner to delete snapshots of interrupted replication steps
|
||||
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
|
||||
If ``encrypted=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
When setting this flag to ``true``, existing step holds for the job will be destroyed on the next replication attempt.
|
||||
Use ``encrypted`` instead of ``raw`` to make your intent clear that zrepl must only replicate filesystems that are actually encrypted by OpenZFS native encryption.
|
||||
It is meant as a safeguard to prevent unintended sends of unencrypted filesystems in raw mode.
|
||||
|
||||
.. _job-send-options-properties:
|
||||
|
||||
``properties``
|
||||
--------------
|
||||
Sends the dataset properties along with snapshots.
|
||||
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||
|
||||
.. _job-send-options-backup-properties:
|
||||
|
||||
``backup_properties``
|
||||
---------------------
|
||||
|
||||
When properties are modified on a filesystem that was received from a send stream with ``send.properties=true``, ZFS archives the original received value internally.
|
||||
This also applies to :ref:`inheriting or overriding properties during zfs receive <job-recv-options--inherit-and-override>`.
|
||||
|
||||
When sending those received filesystems another hop, the ``backup_properties`` flag instructs ZFS to send the original property values rather than the current locally set values.
|
||||
|
||||
This is useful for replicating properties across multiple levels of backup machines.
|
||||
**Example:**
|
||||
Suppose we want to flow snapshots from Machine A to B, then from B to C.
|
||||
A will enable the :ref:`properties send option <job-send-options-properties>`.
|
||||
B will want to override :ref:`critical properties such as mountpoint or canmount <job-note-property-replication>`.
|
||||
But the job that replicates from B to C should be sending the original property values received from A.
|
||||
Thus, B sets the ``backup_properties`` option.
|
||||
|
||||
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||
|
||||
.. _job-send-options-large-blocks:
|
||||
|
||||
``large_blocks``
|
||||
----------------
|
||||
|
||||
This flag should not be changed after initial replication.
|
||||
Prior to `OpenZFS commit 7bcb7f08 <https://github.com/openzfs/zfs/pull/10383/files#diff-4c1e47568f46fb63546e984943b09a3e6b051e2242649523f7835bbdfe2a9110R337-R342>`_
|
||||
it was possible to change this setting which resulted in **data loss on the receiver**.
|
||||
The commit in question is included in OpenZFS 2.0 and works around the problem by prohibiting receives of incremental streams with a flipped setting.
|
||||
|
||||
.. WARNING::
|
||||
|
||||
This bug has **not been fixed in the OpenZFS 0.8 releases** which means that changing this flag after initial replication might cause **data loss** on the receiver.
|
||||
|
||||
.. _job-recv-options:
|
||||
|
||||
Recv Options
|
||||
~~~~~~~~~~~~
|
||||
|
||||
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section.
|
||||
However, there are currently no variables to configure there.
|
||||
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section:
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: pull
|
||||
recv:
|
||||
properties:
|
||||
inherit:
|
||||
- "mountpoint"
|
||||
override: {
|
||||
"org.openzfs.systemd:ignore": "on"
|
||||
}
|
||||
...
|
||||
|
||||
.. _job-recv-options--inherit-and-override:
|
||||
|
||||
``properties``
|
||||
--------------
|
||||
|
||||
``override`` maps directly to the `zfs recv -o flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
||||
Property name-value pairs specified in this map will apply to all received filesystems, regardless of whether the send stream contains properties or not.
|
||||
|
||||
``inherit`` maps directly to the `zfs recv -x flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
||||
Property names specified in this list will be inherited from the receiving side's parent filesystem (e.g. ``root_fs``).
|
||||
|
||||
With both options, the sending side's property value is still stored on the receiver, but the local override or inherit is the one that takes effect.
|
||||
You can send the original properties from the first receiver to another receiver using :ref:`send.backup_properties<job-send-options-backup-properties>`.
|
||||
|
||||
|
||||
.. _job-note-property-replication:
|
||||
|
||||
A Note on Property Replication
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If a send stream contains properties, as per ``send.properties`` or ``send.backup_properties``,
|
||||
the default ZFS behavior is to use those properties on the receiving side, verbatim.
|
||||
|
||||
In many use cases for zrepl, this can have devastating consequences.
|
||||
For example, when backing up a filesystem that has ``mountpoint=/`` to a storage server,
|
||||
that storage server's root filesystem will be shadowed by the received file system on some platforms.
|
||||
Also, many scripts and tools use ZFS user properties for configuration and do not check the property source (``local`` vs. ``received``).
|
||||
If they are installed on the receiving side as well as the sending side, property replication could have unintended effects.
|
||||
|
||||
**zrepl currently does not provide any automatic safe-guards for property replication:**
|
||||
|
||||
* Make sure to read the entire man page on zfs recv (`man zfs recv <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_) before enabling this feature.
|
||||
* Use ``recv.properties.override`` whenever possible, e.g. for ``mountpoint=none`` or ``canmount=off``.
|
||||
* Use ``recv.properties.inherit`` if that makes more sense to you.
|
||||
|
||||
Below is an **non-exhaustive list of problematic properties**.
|
||||
Please open a pull request if you find a property that is missing from this list.
|
||||
(Both with regards to core ZFS tools and other software in the broader ecosystem.)
|
||||
|
||||
Mount behaviour
|
||||
---------------
|
||||
|
||||
* ``mountpoint``
|
||||
* ``canmount``
|
||||
* ``overlay``
|
||||
|
||||
Note: inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
|
||||
This is an `issue in OpenZFS <https://github.com/openzfs/zfs/issues/11416>`_ .
|
||||
As a workaround, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
|
||||
Please comment at zrepl :issue:`430` if you encounter this issue and/or would like zrepl to automatically work around it.
|
||||
|
||||
|
||||
Systemd
|
||||
-------
|
||||
|
||||
With systemd, you should also consider the properties processed by the `zfs-mount-generator <https://manpages.debian.org/buster-backports/zfsutils-linux/zfs-mount-generator.8.en.html>`_ .
|
||||
|
||||
Most notably:
|
||||
|
||||
* ``org.openzfs.systemd:ignore``
|
||||
* ``org.openzfs.systemd:wanted-by``
|
||||
* ``org.openzfs.systemd:required-by``
|
||||
|
||||
Encryption
|
||||
----------
|
||||
|
||||
If the sender filesystems are encrypted but the sender does :ref:`plain sends <zfs-background-knowledge-plain-vs-raw-sends>`
|
||||
and property replication is enabled, the receiver must :ref:`inherit the following properties<job-recv-options--inherit-and-override>`:
|
||||
|
||||
* ``keylocation``
|
||||
* ``keyformat``
|
||||
* ``encryption``
|
||||
|
||||
@@ -15,7 +15,7 @@ A filesystem that does not have snapshots by the snapshotter has lower priority
|
||||
|
||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||
|
||||
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||
Note that the ``zrepl signal replication JOB`` subcommand does not trigger snapshotting.
|
||||
|
||||
|
||||
::
|
||||
@@ -38,7 +38,7 @@ There is also a ``manual`` snapshotting type, which covers the following use cas
|
||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||
* Handling snapshotting through a separate ``snap`` job.
|
||||
|
||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
||||
Note that you will have to trigger replication manually using the ``zrepl signal replication JOB`` subcommand in that case.
|
||||
|
||||
::
|
||||
|
||||
@@ -131,7 +131,7 @@ The following environment variables are set:
|
||||
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
||||
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
||||
|
||||
An empty template hook can be found in :sampleconf:`hooks/template.sh`.
|
||||
An empty template hook can be found in :sampleconf:`/hooks/template.sh`.
|
||||
|
||||
.. _job-hook-type-postgres-checkpoint:
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ The ``tls`` transport uses TCP + TLS with client authentication using client cer
|
||||
The client identity is the common name (CN) presented in the client certificate.
|
||||
|
||||
It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_.
|
||||
For a simple 2-machine setup, see the :ref:`instructions below<transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||
For a simple 2-machine setup, mutual TLS might also be sufficient.
|
||||
We provide :ref:`copy-pastable instructions to generate the certificates below <transport-tcp+tlsclientauth-certgen>`.
|
||||
|
||||
The implementation uses `Go's TLS library <https://golang.org/pkg/crypto/tls/>`_.
|
||||
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
|
||||
@@ -103,6 +104,16 @@ If intermediate CAs are used, the **full chain** must be present in either in th
|
||||
Regardless, the client's certificate must be first in the ``cert`` file, with each following certificate directly certifying the one preceding it (see `TLS's specification <https://tools.ietf.org/html/rfc5246#section-7.4.2>`_).
|
||||
This is the common default when using a CA management tool.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
As of Go 1.15 (zrepl 0.3.0 and newer), the Go TLS / x509 library **requrires Subject Alternative Names**
|
||||
be present in certificates. You might need to re-generate your certificates using one of the :ref:`two alternatives
|
||||
provided below<transport-tcp+tlsclientauth-certgen>`.
|
||||
|
||||
Note further that zrepl continues to use the CommonName field to assign client identities.
|
||||
Hence, we recommend to keep the Subject Alternative Name and the CommonName in sync.
|
||||
|
||||
|
||||
Serve
|
||||
~~~~~
|
||||
|
||||
@@ -147,45 +158,25 @@ The ``server_cn`` specifies the expected common name (CN) of the server's certif
|
||||
It overrides the hostname specified in ``address``.
|
||||
The connection fails if either do not match.
|
||||
|
||||
.. _transport-tcp+tlsclientauth-certgen:
|
||||
|
||||
.. _transport-tcp+tlsclientauth-2machineopenssl:
|
||||
|
||||
Self-Signed Certificates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Mutual-TLS between Two Machines
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make it easy to manage CA infrastructure for multiple clients, e.g. a central zrepl backup server (in sink mode).
|
||||
However, for a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine:
|
||||
|
||||
Suppose you have a push-mode setup, with `backups.example.com` running the :ref:`sink job <job-sink>`, and `prod.example.com` running the :ref:`push job <job-push>`.
|
||||
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 1-5,24
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout HOSTNAME.key \
|
||||
-out HOSTNAME.crt
|
||||
|
||||
#Generating a 4096 bit RSA private key
|
||||
#................++++
|
||||
#.++++
|
||||
#writing new private key to 'backups.key'
|
||||
#-----
|
||||
#You are about to be asked to enter information that will be incorporated
|
||||
#into your certificate request.
|
||||
#What you are about to enter is what is called a Distinguished Name or a DN.
|
||||
#There are quite a few fields but you can leave some blank
|
||||
#For some fields there will be a default value,
|
||||
#If you enter '.', the field will be left blank.
|
||||
#-----
|
||||
#Country Name (2 letter code) [XX]:
|
||||
#State or Province Name (full name) []:
|
||||
#Locality Name (eg, city) [Default City]:
|
||||
#Organization Name (eg, company) [Default Company Ltd]:
|
||||
#Organizational Unit Name (eg, section) []:
|
||||
#Common Name (eg, your name or your server's hostname) []:HOSTNAME
|
||||
#Email Address []:
|
||||
(name=HOSTNAME; openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout $name.key \
|
||||
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
||||
|
||||
Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`.
|
||||
The serve & connect configuration will thus look like the following:
|
||||
@@ -216,6 +207,36 @@ The serve & connect configuration will thus look like the following:
|
||||
...
|
||||
|
||||
|
||||
Certificate Authority using EasyRSA
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For more than two machines, it might make sense to set up a CA infrastructure.
|
||||
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make this very easy:
|
||||
|
||||
::
|
||||
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HOSTS=(backupserver prod1 prod2 prod3)
|
||||
|
||||
curl -L https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.7/EasyRSA-3.0.7.tgz > EasyRSA-3.0.7.tgz
|
||||
echo "157d2e8c115c3ad070c1b2641a4c9191e06a32a8e50971847a718251eeb510a8 EasyRSA-3.0.7.tgz" | sha256sum -c
|
||||
rm -rf EasyRSA-3.0.7
|
||||
tar -xf EasyRSA-3.0.7.tgz
|
||||
cd EasyRSA-3.0.7
|
||||
./easyrsa
|
||||
./easyrsa init-pki
|
||||
./easyrsa build-ca nopass
|
||||
|
||||
for host in "${HOSTS[@]}"; do
|
||||
./easyrsa build-serverClient-full $host nopass
|
||||
echo cert for host $host available at pki/issued/$host.crt
|
||||
echo key for host $host available at pki/private/$host.key
|
||||
done
|
||||
echo ca cert available at pki/ca.crt
|
||||
|
||||
|
||||
.. _transport-ssh+stdinserver:
|
||||
|
||||
``ssh+stdinserver`` Transport
|
||||
|
||||
@@ -58,13 +58,11 @@ for latest_patch in latest_by_major_minor:
|
||||
cmdline.append("--whitelist-tags")
|
||||
cmdline.append(f"^{re.escape(latest_patch.orig)}$")
|
||||
|
||||
# we want to render the latest non-rc version as the default page
|
||||
# (latest_by_major_minor is already sorted)
|
||||
default_version = latest_by_major_minor[-1]
|
||||
for tag in reversed(latest_by_major_minor):
|
||||
if tag.rc == 0:
|
||||
default_version = tag
|
||||
break
|
||||
# we want flexibility to update docs for the latest stable release
|
||||
# => we have a branch for that, called `stable` which we move manually
|
||||
# TODO: in the future, have f"stable-{latest_by_major_minor[-1]}"
|
||||
default_version = "stable"
|
||||
cmdline.extend(["--whitelist-branches", default_version])
|
||||
|
||||
cmdline.extend(["--root-ref", f"{default_version}"])
|
||||
cmdline.extend(["--banner-main-ref", f"{default_version}"])
|
||||
@@ -73,4 +71,4 @@ cmdline.extend(["--sort", "semver"])
|
||||
|
||||
cmdline.extend(["--whitelist-branches", "master"])
|
||||
|
||||
print(" ".join(cmdline))
|
||||
print(" ".join(cmdline))
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
||||
:target: https://liberapay.com/zrepl/donate
|
||||
.. |Donate via Patreon| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow
|
||||
.. |Donate via Patreon| image:: https://img.shields.io/badge/dynamic/json?color=yellow&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F3095079
|
||||
:target: https://www.patreon.com/zrepl
|
||||
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
||||
:target: https://github.com/sponsors/problame
|
||||
|
||||
+5
-1
@@ -61,7 +61,11 @@ Main Features
|
||||
* [x] Automatic ZFS holds during send & receive
|
||||
* [x] Automatic bookmark \& hold management for guaranteed incremental send & recv
|
||||
* [x] Encrypted raw send & receive to untrusted receivers (OpenZFS native encryption)
|
||||
* [ ] Compressed send & receive
|
||||
* [x] Properties send & receive
|
||||
* [x] Compressed send & receive
|
||||
* [x] Large blocks send & receive
|
||||
* [x] Embedded data send & receive
|
||||
* [x] Resume state send & receive
|
||||
|
||||
* **Automatic snapshot management**
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ Installation
|
||||
installation/user-privileges
|
||||
installation/packages
|
||||
installation/apt-repos
|
||||
installation/rpm-repos
|
||||
installation/compile-from-source
|
||||
installation/freebsd-jail-with-iocage
|
||||
installation/what-next
|
||||
|
||||
@@ -7,7 +7,7 @@ Debian / Ubuntu APT repositories
|
||||
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
||||
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
||||
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
|
||||
Please open an issue in on GitHub if you encounter any issues with the repository.
|
||||
|
||||
The following snippet configure the repository for your Debian or Ubuntu release:
|
||||
|
||||
@@ -24,6 +24,6 @@ The following snippet configure the repository for your Debian or Ubuntu release
|
||||
|
||||
.. NOTE::
|
||||
|
||||
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
||||
Until zrepl reaches 1.0, the repositories will be updated to the latest zrepl release immediately.
|
||||
This includes breaking changes between zrepl versions.
|
||||
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||
|
||||
@@ -9,37 +9,35 @@ Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** +
|
||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||
Python and pip3 should probably be installed via your distro's package manager.
|
||||
|
||||
::
|
||||
cd to/your/zrepl/checkout
|
||||
python3 -m venv3
|
||||
source venv3/bin/activate
|
||||
./lazy.sh devsetup
|
||||
make release
|
||||
# build artifacts are available in ./artifacts/release
|
||||
|
||||
The Python venv is used for the documentation build dependencies.
|
||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||
|
||||
Alternatively, you can use the Docker build process:
|
||||
it is used to produce the official zrepl `binary releases`_
|
||||
and serves as a reference for build dependencies and procedure:
|
||||
|
||||
::
|
||||
|
||||
git clone https://github.com/zrepl/zrepl.git && \
|
||||
cd zrepl && \
|
||||
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
||||
sudo docker run -it --rm \
|
||||
-v "${PWD}:/src" \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
zrepl_build make release
|
||||
cd to/your/zrepl/checkout
|
||||
# make sure your user has access to the docker socket
|
||||
make release-docker
|
||||
# if you want .deb or .rpm packages, invoke the follwoing
|
||||
# targets _after_ you invoked release-docker
|
||||
make deb-docker
|
||||
make rpm-docker
|
||||
# build artifacts are available in ./artifacts/release
|
||||
# packages are available in ./artifacts
|
||||
|
||||
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
||||
|
||||
::
|
||||
|
||||
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||
python3 -m venv3
|
||||
source venv3/bin/activate
|
||||
./lazy.sh devsetup
|
||||
make release
|
||||
|
||||
The Python venv is used for the documentation build dependencies.
|
||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||
Either way, all build results are located in the ``artifacts/`` directory.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||
It is your job to install the built binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||
Otherwise, the examples in the :ref:`quick-start guides <quickstart-toc>` may need to be adjusted.
|
||||
|
||||
@@ -30,15 +30,12 @@ The following list may be incomplete, feel free to submit a PR with an update:
|
||||
* - Arch Linux
|
||||
- ``yay install zrepl``
|
||||
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||
* - Fedora
|
||||
* - Fedora, CentOS, RHEL, OpenSUSE
|
||||
- ``dnf install zrepl``
|
||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||
* - CentOS/RHEL
|
||||
- ``yum install zrepl``
|
||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||
- :ref:`RPM repository config <installation-rpm-repos>`
|
||||
* - Debian + Ubuntu
|
||||
- ``apt install zrepl``
|
||||
- APT repository config :ref:`see below <installation-apt-repos>`
|
||||
- :ref:`APT repository config <installation-apt-repos>`
|
||||
* - OmniOS
|
||||
- ``pkg install zrepl``
|
||||
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.. _installation-rpm-repos:
|
||||
|
||||
RPM repositories
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
We provide a single RPM repository for all RPM-based Linux distros.
|
||||
The zrepl binary in the repo is the same as the one published to GitHub.
|
||||
Since Go binaries are statically linked, the RPM should work about everywhere.
|
||||
|
||||
The fingerprint of the signing key is ``F6F6 E8EA 6F2F 1462 2878 B5DE 50E3 4417 826E 2CE6``.
|
||||
It is available at `<https://zrepl.cschwarz.com/rpm/rpm-key.asc>`_ .
|
||||
Please open an issue on GitHub if you encounter any issues with the repository.
|
||||
|
||||
Copy-paste the following snippet into your shell to set up the zrepl repository.
|
||||
Then ``dnf install zrepl`` and make sure to confirm that the signing key matches the one shown above.
|
||||
|
||||
::
|
||||
|
||||
cat > /etc/yum.repos.d/zrepl.repo <<EOF
|
||||
[zrepl]
|
||||
name = zrepl
|
||||
baseurl = https://zrepl.cschwarz.com/rpm/repo
|
||||
gpgkey = https://zrepl.cschwarz.com/rpm/rpm-key.asc
|
||||
EOF
|
||||
|
||||
.. NOTE::
|
||||
|
||||
Until zrepl reaches 1.0, the repository will be updated to the latest zrepl release immediately.
|
||||
This includes breaking changes between zrepl versions.
|
||||
If that bothers you, use the `dnf versionlock plugin <https://dnf-plugins-core.readthedocs.io/en/latest/versionlock.html>`_ to pin the version of zrepl on your system.
|
||||
+32
-7
@@ -1,14 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
|
||||
NON_INTERACTIVE=false
|
||||
DO_CLONE=false
|
||||
while getopts "ca" arg; do
|
||||
case "$arg" in
|
||||
"a")
|
||||
NON_INTERACTIVE=true
|
||||
;;
|
||||
"c")
|
||||
DO_CLONE=true
|
||||
;;
|
||||
*)
|
||||
echo invalid option
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git"
|
||||
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
PUBLICDIR="${SCRIPTDIR}/public_git"
|
||||
|
||||
checkout_repo_msg() {
|
||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||
echo " git clone ${GHPAGESREPO} ${PUBLICDIR}"
|
||||
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
|
||||
}
|
||||
|
||||
exit_msg() {
|
||||
@@ -25,11 +41,19 @@ cd "$SCRIPTDIR"
|
||||
|
||||
if [ ! -d "$PUBLICDIR" ]; then
|
||||
checkout_repo_msg
|
||||
exit 1
|
||||
if $DO_CLONE; then
|
||||
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -n "PRESS ENTER to confirm you commited and pushed docs changes and tags to the zrepl repo"
|
||||
read
|
||||
if $NON_INTERACTIVE; then
|
||||
echo "non-interactive mode"
|
||||
else
|
||||
echo -n "PRESS ENTER to confirm you commited and pushed docs changes and tags to the zrepl repo"
|
||||
read -r
|
||||
fi
|
||||
|
||||
pushd "$PUBLICDIR"
|
||||
|
||||
@@ -52,9 +76,10 @@ popd
|
||||
|
||||
echo "building site"
|
||||
|
||||
flags="$(python3 gen-sphinx-versioning-flags.py)"
|
||||
set -e
|
||||
sphinx-versioning build \
|
||||
$(python3 gen-sphinx-versioning-flags.py) \
|
||||
$flags \
|
||||
docs ./public_git \
|
||||
-- -c sphinxconf # older conf.py throw errors because they used
|
||||
# version = subprocess.show_output(["git", "describe"])
|
||||
@@ -66,7 +91,7 @@ git status --porcelain
|
||||
if [[ "$(git status --porcelain)" != "" ]]; then
|
||||
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
|
||||
fi
|
||||
COMMIT_MSG="sphinx-versioning render from publish.sh - `date -u` - ${CURRENT_COMMIT}"
|
||||
COMMIT_MSG="sphinx-versioning render from publish.sh - $(date -u) - ${CURRENT_COMMIT}"
|
||||
|
||||
pushd "$PUBLICDIR"
|
||||
|
||||
|
||||
+2
-2
@@ -55,8 +55,8 @@ Watch it Work
|
||||
=============
|
||||
|
||||
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup JOBNAME``.
|
||||
(refer to the example use case document if you are uncertain which job you want to wake up).
|
||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal replication JOBNAME``.
|
||||
(refer to the example use case document if you are uncertain which job you want to start replication).
|
||||
|
||||
You can also use basic UNIX tools to inspect see what's going on.
|
||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||
|
||||
@@ -25,7 +25,7 @@ A few additional requirements:
|
||||
* We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation.
|
||||
**zrepl should use bookmarks and holds to achieve this goal**.
|
||||
* When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time.
|
||||
Therefore, we want zrepl to deviate from its :ref:`default step-hold behavior <step-holds-and-bookmarks>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation.
|
||||
Therefore, we want zrepl to deviate from its :ref:`default behavior <replication-option-protection>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation.
|
||||
**zrepl should provide an easy config knob to disable step holds for incremental replication**.
|
||||
|
||||
The following config snippet implements the setup described above.
|
||||
|
||||
@@ -51,21 +51,18 @@ To get things going quickly, we skip setting up a CA and generate two self-signe
|
||||
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 6,13
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout backups.key \
|
||||
-out backups.crt
|
||||
# ... and use "backups" as Common Name (CN)
|
||||
(name=backups; openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout $name.key \
|
||||
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
||||
|
||||
openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout prod.key \
|
||||
-out prod.crt
|
||||
# ... and use "prod" as Common Name (CN)
|
||||
(name=prod; openssl req -x509 -sha256 -nodes \
|
||||
-newkey rsa:4096 \
|
||||
-days 365 \
|
||||
-keyout $name.key \
|
||||
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
||||
|
||||
ssh root@backups "mkdir /etc/zrepl"
|
||||
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
||||
@@ -94,4 +91,4 @@ We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`
|
||||
Go Back To Quickstart Guide
|
||||
---------------------------
|
||||
|
||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||
|
||||
+6
-1
@@ -32,6 +32,11 @@ We would like to thank the following people and organizations for supporting zre
|
||||
|
||||
<div class="fa fa-code" style="width: 1em;"></div>
|
||||
|
||||
* |supporter-gold| Cyberiada GmbH
|
||||
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||
* |supporter-std| `Michael D. Schmitt <https://waterbendingscroll.dancingdragons.org>`_
|
||||
* |supporter-std| `Hans Schulz <https://github.com/schulzh>`_
|
||||
* |supporter-std| Henning Kessler
|
||||
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
|
||||
* |supporter-std| `DrLuke <https://github.com/drluke>`_
|
||||
@@ -43,7 +48,7 @@ We would like to thank the following people and organizations for supporting zre
|
||||
* |supporter-gold| `MNX.io <https://mnx.io>`_
|
||||
* |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_
|
||||
* |supporter-code| `Ross Williams <https://github.com/overhacked>`_
|
||||
* |supporter-std| Mike T.
|
||||
* |supporter-gold| Mike T.
|
||||
* |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_
|
||||
* |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_
|
||||
* |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ CLI Overview
|
||||
The zrepl binary is self-documenting:
|
||||
run ``zrepl help`` for an overview of the available subcommands or ``zrepl SUBCOMMAND --help`` for information on available flags, etc.
|
||||
|
||||
.. _cli-signal-wakeup:
|
||||
.. _cli-signal-replication:
|
||||
|
||||
.. list-table::
|
||||
:widths: 30 70
|
||||
@@ -29,7 +29,7 @@ CLI Overview
|
||||
- show job activity, or with ``--raw`` for JSON output
|
||||
* - ``zrepl stdinserver``
|
||||
- see :ref:`transport-ssh+stdinserver`
|
||||
* - ``zrepl signal wakeup JOB``
|
||||
* - ``zrepl signal replication JOB``
|
||||
- manually trigger replication + pruning of JOB
|
||||
* - ``zrepl signal reset JOB``
|
||||
- manually abort current replication + pruning of JOB
|
||||
|
||||
+132
-169
@@ -10,34 +10,35 @@ import (
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/util/chainedio"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/semaphore"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||
)
|
||||
|
||||
type SenderConfig struct {
|
||||
FSF zfs.DatasetFilter
|
||||
DisableIncrementalStepHolds bool
|
||||
JobID JobID
|
||||
FSF zfs.DatasetFilter
|
||||
JobID JobID
|
||||
|
||||
Encrypt *zfs.NilBool
|
||||
Encrypt *nodefault.Bool
|
||||
SendRaw bool
|
||||
SendProperties bool
|
||||
SendBackupProperties bool
|
||||
SendLargeBlocks bool
|
||||
SendCompressed bool
|
||||
SendEmbeddedData bool
|
||||
SendSaved bool
|
||||
}
|
||||
|
||||
func (c *SenderConfig) Validate() error {
|
||||
c.JobID.MustValidate()
|
||||
if err := c.Encrypt.Validate(); err != nil {
|
||||
if err := c.Encrypt.ValidateNoDefault(); err != nil {
|
||||
return errors.Wrap(err, "`Encrypt` field invalid")
|
||||
}
|
||||
if _, err := StepHoldTag(c.JobID); err != nil {
|
||||
@@ -48,17 +49,11 @@ func (c *SenderConfig) Validate() error {
|
||||
|
||||
// Sender implements replication.ReplicationEndpoint for a sending side
|
||||
type Sender struct {
|
||||
FSFilter zfs.DatasetFilter
|
||||
disableIncrementalStepHolds bool
|
||||
jobId JobID
|
||||
pdu.UnsafeReplicationServer // prefer compilation errors over default 'method X not implemented' impl
|
||||
|
||||
encrypt *zfs.NilBool
|
||||
sendRaw bool
|
||||
sendProperties bool
|
||||
sendBackupProperties bool
|
||||
sendLargeBlocks bool
|
||||
sendCompressed bool
|
||||
sendEmbeddedData bool
|
||||
FSFilter zfs.DatasetFilter
|
||||
jobId JobID
|
||||
config SenderConfig
|
||||
}
|
||||
|
||||
func NewSender(conf SenderConfig) *Sender {
|
||||
@@ -66,17 +61,9 @@ func NewSender(conf SenderConfig) *Sender {
|
||||
panic("invalid config" + err.Error())
|
||||
}
|
||||
return &Sender{
|
||||
FSFilter: conf.FSF,
|
||||
disableIncrementalStepHolds: conf.DisableIncrementalStepHolds,
|
||||
jobId: conf.JobID,
|
||||
|
||||
encrypt: conf.Encrypt,
|
||||
sendRaw: conf.SendRaw,
|
||||
sendProperties: conf.SendProperties,
|
||||
sendBackupProperties: conf.SendBackupProperties,
|
||||
sendLargeBlocks: conf.SendLargeBlocks,
|
||||
sendCompressed: conf.SendCompressed,
|
||||
sendEmbeddedData: conf.SendEmbeddedData,
|
||||
FSFilter: conf.FSF,
|
||||
jobId: conf.JobID,
|
||||
config: conf,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +129,6 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
||||
|
||||
}
|
||||
|
||||
var maxConcurrentZFSSend = envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10)
|
||||
var maxConcurrentZFSSendSemaphore = semaphore.New(maxConcurrentZFSSend)
|
||||
|
||||
func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion {
|
||||
if fsv == nil {
|
||||
return nil
|
||||
@@ -176,12 +160,12 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
// use s.encrypt setting
|
||||
// ok, fallthrough outer
|
||||
case pdu.Tri_False:
|
||||
if s.encrypt.B {
|
||||
if s.config.Encrypt.B {
|
||||
return nil, nil, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
|
||||
}
|
||||
// fallthrough outer
|
||||
case pdu.Tri_True:
|
||||
if !s.encrypt.B {
|
||||
if !s.config.Encrypt.B {
|
||||
return nil, nil, errors.New("only unencrypted sends allowed, but encrypted send requested")
|
||||
}
|
||||
// fallthrough outer
|
||||
@@ -190,18 +174,20 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
}
|
||||
|
||||
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: r.Filesystem,
|
||||
From: uncheckedSendArgsFromPDU(r.GetFrom()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||
To: uncheckedSendArgsFromPDU(r.GetTo()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||
ResumeToken: r.ResumeToken, // nil or not nil, depending on decoding success
|
||||
|
||||
Encrypted: s.encrypt,
|
||||
Properties: s.sendProperties,
|
||||
BackupProperties: s.sendBackupProperties,
|
||||
Raw: s.sendRaw,
|
||||
LargeBlocks: s.sendLargeBlocks,
|
||||
Compressed: s.sendCompressed,
|
||||
EmbeddedData: s.sendEmbeddedData,
|
||||
FS: r.Filesystem,
|
||||
From: uncheckedSendArgsFromPDU(r.GetFrom()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||
To: uncheckedSendArgsFromPDU(r.GetTo()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||
ResumeToken: r.ResumeToken, // nil or not nil, depending on decoding success
|
||||
Encrypted: s.config.Encrypt,
|
||||
Properties: s.config.SendProperties,
|
||||
BackupProperties: s.config.SendBackupProperties,
|
||||
Raw: s.config.SendRaw,
|
||||
LargeBlocks: s.config.SendLargeBlocks,
|
||||
Compressed: s.config.SendCompressed,
|
||||
EmbeddedData: s.config.SendEmbeddedData,
|
||||
Saved: s.config.SendSaved,
|
||||
},
|
||||
}
|
||||
|
||||
sendArgs, err := sendArgsUnvalidated.Validate(ctx)
|
||||
@@ -209,31 +195,6 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
return nil, nil, errors.Wrap(err, "validate send arguments")
|
||||
}
|
||||
|
||||
if s.sendRaw {
|
||||
encryptionSupported, err := zfs.EncryptionCLISupported(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot determine CLI native encryption support")
|
||||
}
|
||||
|
||||
if !encryptionSupported {
|
||||
// zfs-send(8) about `send -w`: "For unencrypted datasets, this flag will be equivalent to -Lec."
|
||||
sendArgs.Raw = false
|
||||
sendArgs.LargeBlocks = true // -L
|
||||
sendArgs.Compressed = true // -c
|
||||
sendArgs.EmbeddedData = true // -e
|
||||
}
|
||||
}
|
||||
|
||||
getLogger(ctx).Debug("acquire concurrent send semaphore")
|
||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||
// => would require handling on the client-side
|
||||
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
|
||||
guard, err := maxConcurrentZFSSendSemaphore.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "zfs send dry failed")
|
||||
@@ -255,37 +216,27 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
return res, nil, nil
|
||||
}
|
||||
|
||||
// create a replication cursor for `From` (usually an idempotent no-op because SendCompleted already created it before)
|
||||
var fromReplicationCursor Abstraction
|
||||
if sendArgs.From != nil {
|
||||
// For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor
|
||||
fromReplicationCursor, err = CreateReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
||||
// fallthrough
|
||||
} else if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot set replication cursor to `from` version before starting send")
|
||||
}
|
||||
// create holds or bookmarks of `From` and `To` to guarantee one of the following:
|
||||
// - that the replication step can always be resumed (`holds`),
|
||||
// - that the replication step can be interrupted and a future replication
|
||||
// step with same or different `To` but same `From` is still possible (`bookmarks`)
|
||||
// - nothing (`none`)
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// ... actually create the abstractions
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(r.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
takeStepHolds := sendArgs.FromVersion == nil || !s.disableIncrementalStepHolds
|
||||
|
||||
var fromHold, toHold Abstraction
|
||||
// make sure `From` doesn't go away in order to make this step resumable
|
||||
if sendArgs.From != nil && takeStepHolds {
|
||||
fromHold, err = HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it")
|
||||
// fallthrough
|
||||
} else if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot hold `from` version %q before starting send", *sendArgs.FromVersion)
|
||||
}
|
||||
replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(sendArgs.From != nil)
|
||||
liveAbs, err := replicationGuaranteeStrategy.SenderPreSend(ctx, s.jobId, &sendArgs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if takeStepHolds {
|
||||
// make sure `To` doesn't go away in order to make this step resumable
|
||||
toHold, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion)
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +256,6 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
//
|
||||
// Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep,
|
||||
// will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup.
|
||||
liveAbs := []Abstraction{fromHold, toHold, fromReplicationCursor}
|
||||
func() {
|
||||
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
|
||||
defer endSpan()
|
||||
@@ -327,22 +277,29 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
for _, staleVersion := range obsoleteAbs {
|
||||
for _, mustLiveVersion := range mustLiveVersions {
|
||||
isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion())
|
||||
isStepHoldWeMightHaveCreatedWithCurrentValueOf_takeStepHolds :=
|
||||
takeStepHolds && staleVersion.GetType() == AbstractionStepHold
|
||||
if isSendArg && isStepHoldWeMightHaveCreatedWithCurrentValueOf_takeStepHolds {
|
||||
stepHoldBasedGuaranteeStrategy := false
|
||||
k := replicationGuaranteeStrategy.Kind()
|
||||
switch k {
|
||||
case ReplicationGuaranteeKindResumability:
|
||||
stepHoldBasedGuaranteeStrategy = true
|
||||
case ReplicationGuaranteeKindIncremental:
|
||||
case ReplicationGuaranteeKindNone:
|
||||
default:
|
||||
panic(fmt.Sprintf("this is supposed to be an exhaustive match, got %v", k))
|
||||
}
|
||||
isSnapshot := mustLiveVersion.IsSnapshot()
|
||||
if isSendArg && (!isSnapshot || stepHoldBasedGuaranteeStrategy) {
|
||||
panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, keep, check)
|
||||
}()
|
||||
// now add the newly created abstractions to the cleaned-up cache
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
sendAbstractionsCacheSingleton.Put(a)
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
}
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
||||
}()
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
if err != nil {
|
||||
@@ -376,36 +333,32 @@ func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*p
|
||||
return nil, errors.Wrap(err, "validate `to` exists")
|
||||
}
|
||||
|
||||
log := func(ctx context.Context) Logger {
|
||||
log := getLogger(ctx).WithField("to_guid", to.Guid).
|
||||
WithField("fs", fs).
|
||||
WithField("to", to.RelName)
|
||||
if from != nil {
|
||||
log = log.WithField("from", from.RelName).WithField("from_guid", from.Guid)
|
||||
}
|
||||
return log
|
||||
}
|
||||
|
||||
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, p.jobId)
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(orig.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
log(ctx).Debug("not setting replication cursor, bookmark cloning not supported")
|
||||
} else {
|
||||
msg := "cannot move replication cursor, keeping hold on `to` until successful"
|
||||
log(ctx).WithError(err).Error(msg)
|
||||
err = errors.Wrap(err, msg)
|
||||
// it is correct to not destroy from and to step holds if we can't move the cursor!
|
||||
return &pdu.SendCompletedRes{}, err
|
||||
return nil, err
|
||||
}
|
||||
liveAbs, err := replicationGuaranteeOptions.Strategy(from != nil).SenderPostRecvConfirmed(ctx, p.jobId, fs, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
}
|
||||
} else {
|
||||
sendAbstractionsCacheSingleton.Put(toReplicationCursor)
|
||||
log(ctx).WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
|
||||
}
|
||||
|
||||
keep := func(a Abstraction) bool {
|
||||
return AbstractionEquals(a, toReplicationCursor)
|
||||
keep := func(a Abstraction) (keep bool) {
|
||||
keep = false
|
||||
for _, k := range liveAbs {
|
||||
keep = keep || AbstractionEquals(a, k)
|
||||
}
|
||||
return keep
|
||||
}
|
||||
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, keep, nil)
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, destroyTypes, keep, nil)
|
||||
|
||||
return &pdu.SendCompletedRes{}, nil
|
||||
|
||||
@@ -481,11 +434,9 @@ type FSMap interface { // FIXME unused
|
||||
type ReceiverConfig struct {
|
||||
JobID JobID
|
||||
|
||||
RootWithoutClientComponent *zfs.DatasetPath // TODO use
|
||||
RootWithoutClientComponent *zfs.DatasetPath
|
||||
AppendClientIdentity bool
|
||||
|
||||
UpdateLastReceivedHold bool
|
||||
|
||||
InheritProperties []zfsprop.Property
|
||||
OverrideProperties map[zfsprop.Property]string
|
||||
}
|
||||
@@ -507,21 +458,17 @@ func (c *ReceiverConfig) copyIn() {
|
||||
func (c *ReceiverConfig) Validate() error {
|
||||
c.JobID.MustValidate()
|
||||
|
||||
if c.InheritProperties != nil {
|
||||
for _, prop := range c.InheritProperties {
|
||||
err := prop.Validate()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "inherit property %q", prop)
|
||||
}
|
||||
for _, prop := range c.InheritProperties {
|
||||
err := prop.Validate()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "inherit property %q", prop)
|
||||
}
|
||||
}
|
||||
|
||||
if c.OverrideProperties != nil {
|
||||
for prop := range c.OverrideProperties {
|
||||
err := prop.Validate()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "override property %q", prop)
|
||||
}
|
||||
for prop := range c.OverrideProperties {
|
||||
err := prop.Validate()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "override property %q", prop)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,6 +480,8 @@ func (c *ReceiverConfig) Validate() error {
|
||||
|
||||
// Receiver implements replication.ReplicationEndpoint for a receiving side
|
||||
type Receiver struct {
|
||||
pdu.UnsafeReplicationServer // prefer compilation errors over default 'method X not implemented' impl
|
||||
|
||||
conf ReceiverConfig // validated
|
||||
|
||||
recvParentCreationMtx *chainlock.L
|
||||
@@ -574,7 +523,7 @@ func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
||||
|
||||
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("ClientIdentityKey context value must be set"))
|
||||
panic("ClientIdentityKey context value must be set")
|
||||
}
|
||||
|
||||
clientRoot, err := clientRoot(s.conf.RootWithoutClientComponent, clientIdentity)
|
||||
@@ -719,8 +668,6 @@ func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io
|
||||
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
||||
}
|
||||
|
||||
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10))
|
||||
|
||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
@@ -757,7 +704,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
f := zfs.NewDatasetPathForest()
|
||||
f.Add(lp)
|
||||
getLogger(ctx).Debug("begin tree-walk")
|
||||
f.WalkTopDown(func(v zfs.DatasetPathVisit) (visitChildTree bool) {
|
||||
f.WalkTopDown(func(v *zfs.DatasetPathVisit) (visitChildTree bool) {
|
||||
if v.Path.Equal(lp) {
|
||||
return false
|
||||
}
|
||||
@@ -785,7 +732,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
}
|
||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||
l.Debug("create placeholder filesystem")
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path)
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path, v.Parent.Path)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
||||
visitErr = err
|
||||
@@ -840,16 +787,6 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
||||
}
|
||||
|
||||
log.Debug("acquire concurrent recv semaphore")
|
||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||
// => would require handling on the client-side
|
||||
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
|
||||
guard, err := maxConcurrentZFSRecvSemaphore.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
var peek bytes.Buffer
|
||||
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
||||
@@ -945,12 +882,38 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
||||
return nil, errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
if s.conf.UpdateLastReceivedHold {
|
||||
log.Debug("move last-received-hold")
|
||||
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot move last-received-hold")
|
||||
replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(req.GetReplicationConfig().Protection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(ph.FSExists)
|
||||
liveAbs, err := replicationGuaranteeStrategy.ReceiverPostRecv(ctx, s.conf.JobID, lp.ToString(), toRecvd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range liveAbs {
|
||||
if a != nil {
|
||||
abstractionsCacheSingleton.Put(a)
|
||||
}
|
||||
}
|
||||
keep := func(a Abstraction) (keep bool) {
|
||||
keep = false
|
||||
for _, k := range liveAbs {
|
||||
keep = keep || AbstractionEquals(a, k)
|
||||
}
|
||||
return keep
|
||||
}
|
||||
check := func(obsoleteAbs []Abstraction) {
|
||||
for _, abs := range obsoleteAbs {
|
||||
if zfs.FilesystemVersionEqualIdentity(abs.GetFilesystemVersion(), toRecvd) {
|
||||
panic(fmt.Sprintf("would destroy endpoint abstraction around the filesystem version we just received %s", abs))
|
||||
}
|
||||
}
|
||||
}
|
||||
destroyTypes := AbstractionTypeSet{
|
||||
AbstractionLastReceivedHold: true,
|
||||
}
|
||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.conf.JobID, lp.ToString(), destroyTypes, keep, check)
|
||||
|
||||
return &pdu.ReceiveRes{}, nil
|
||||
}
|
||||
|
||||
+44
-42
@@ -6,53 +6,54 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
)
|
||||
|
||||
var sendAbstractionsCacheMetrics struct {
|
||||
var abstractionsCacheMetrics struct {
|
||||
count prometheus.Gauge
|
||||
}
|
||||
|
||||
func init() {
|
||||
sendAbstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
abstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "endpoint",
|
||||
Name: "send_abstractions_cache_entry_count",
|
||||
Help: "number of send abstractions tracked in the sendAbstractionsCache data structure",
|
||||
Name: "abstractions_cache_entry_count",
|
||||
Help: "number of abstractions tracked in the abstractionsCache data structure",
|
||||
})
|
||||
}
|
||||
|
||||
var sendAbstractionsCacheSingleton = newSendAbstractionsCache()
|
||||
var abstractionsCacheSingleton = newAbstractionsCache()
|
||||
|
||||
func SendAbstractionsCacheInvalidate(fs string) {
|
||||
sendAbstractionsCacheSingleton.InvalidateFSCache(fs)
|
||||
func AbstractionsCacheInvalidate(fs string) {
|
||||
abstractionsCacheSingleton.InvalidateFSCache(fs)
|
||||
}
|
||||
|
||||
type sendAbstractionsCacheDidLoadFSState int
|
||||
type abstractionsCacheDidLoadFSState int
|
||||
|
||||
const (
|
||||
sendAbstractionsCacheDidLoadFSStateNo sendAbstractionsCacheDidLoadFSState = iota // 0-value has meaning
|
||||
sendAbstractionsCacheDidLoadFSStateInProgress
|
||||
sendAbstractionsCacheDidLoadFSStateDone
|
||||
abstractionsCacheDidLoadFSStateNo abstractionsCacheDidLoadFSState = iota // 0-value has meaning
|
||||
abstractionsCacheDidLoadFSStateInProgress
|
||||
abstractionsCacheDidLoadFSStateDone
|
||||
)
|
||||
|
||||
type sendAbstractionsCache struct {
|
||||
type abstractionsCache struct {
|
||||
mtx chainlock.L
|
||||
abstractions []Abstraction
|
||||
didLoadFS map[string]sendAbstractionsCacheDidLoadFSState
|
||||
didLoadFS map[string]abstractionsCacheDidLoadFSState
|
||||
didLoadFSChanged *sync.Cond
|
||||
}
|
||||
|
||||
func newSendAbstractionsCache() *sendAbstractionsCache {
|
||||
c := &sendAbstractionsCache{
|
||||
didLoadFS: make(map[string]sendAbstractionsCacheDidLoadFSState),
|
||||
func newAbstractionsCache() *abstractionsCache {
|
||||
c := &abstractionsCache{
|
||||
didLoadFS: make(map[string]abstractionsCacheDidLoadFSState),
|
||||
}
|
||||
c.didLoadFSChanged = c.mtx.NewCond()
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *sendAbstractionsCache) Put(a Abstraction) {
|
||||
func (s *abstractionsCache) Put(a Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
|
||||
var zeroJobId JobID
|
||||
@@ -63,10 +64,10 @@ func (s *sendAbstractionsCache) Put(a Abstraction) {
|
||||
}
|
||||
|
||||
s.abstractions = append(s.abstractions, a)
|
||||
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
}
|
||||
|
||||
func (s *sendAbstractionsCache) InvalidateFSCache(fs string) {
|
||||
func (s *abstractionsCache) InvalidateFSCache(fs string) {
|
||||
// FIXME: O(n)
|
||||
newAbs := make([]Abstraction, 0, len(s.abstractions))
|
||||
for _, a := range s.abstractions {
|
||||
@@ -75,9 +76,9 @@ func (s *sendAbstractionsCache) InvalidateFSCache(fs string) {
|
||||
}
|
||||
}
|
||||
s.abstractions = newAbs
|
||||
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
|
||||
s.didLoadFSChanged.Broadcast()
|
||||
|
||||
}
|
||||
@@ -86,7 +87,7 @@ func (s *sendAbstractionsCache) InvalidateFSCache(fs string) {
|
||||
// - only fetches on-disk abstractions once, but every time from the in-memory store
|
||||
//
|
||||
// That means that for precise results, all abstractions created by the endpoint must be .Put into this cache.
|
||||
func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string, keep func(a Abstraction) bool) (ret []Abstraction) {
|
||||
func (s *abstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool) (ret []Abstraction) {
|
||||
defer s.mtx.Lock().Unlock()
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
var zeroJobId JobID
|
||||
@@ -97,50 +98,50 @@ func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jo
|
||||
panic("must not pass zero-value fs")
|
||||
}
|
||||
|
||||
s.tryLoadOnDiskSendAbstractions(ctx, fs)
|
||||
s.tryLoadOnDiskAbstractions(ctx, fs)
|
||||
|
||||
// FIXME O(n)
|
||||
var remaining []Abstraction
|
||||
for _, a := range s.abstractions {
|
||||
aJobId := *a.GetJobID()
|
||||
aFS := a.GetFS()
|
||||
if aJobId == jobID && aFS == fs && !keep(a) {
|
||||
if aJobId == jobID && aFS == fs && types[a.GetType()] && !keep(a) {
|
||||
ret = append(ret, a)
|
||||
} else {
|
||||
remaining = append(remaining, a)
|
||||
}
|
||||
}
|
||||
s.abstractions = remaining
|
||||
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
abstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// caller must hold s.mtx
|
||||
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Context, fs string) {
|
||||
for s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateDone {
|
||||
if s.didLoadFS[fs] == sendAbstractionsCacheDidLoadFSStateInProgress {
|
||||
func (s *abstractionsCache) tryLoadOnDiskAbstractions(ctx context.Context, fs string) {
|
||||
for s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateDone {
|
||||
if s.didLoadFS[fs] == abstractionsCacheDidLoadFSStateInProgress {
|
||||
s.didLoadFSChanged.Wait()
|
||||
continue
|
||||
}
|
||||
if s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateNo {
|
||||
if s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateNo {
|
||||
panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs]))
|
||||
}
|
||||
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateInProgress
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateInProgress
|
||||
defer s.didLoadFSChanged.Broadcast()
|
||||
|
||||
var onDiskAbs []Abstraction
|
||||
var err error
|
||||
s.mtx.DropWhile(func() {
|
||||
onDiskAbs, err = s.tryLoadOnDiskSendAbstractionsImpl(ctx, fs) // no shadow
|
||||
onDiskAbs, err = s.tryLoadOnDiskAbstractionsImpl(ctx, fs) // no shadow
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo
|
||||
getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list send step abstractions for filesystem")
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo
|
||||
getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list abstractions for filesystem")
|
||||
} else {
|
||||
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateDone
|
||||
s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateDone
|
||||
s.abstractions = append(s.abstractions, onDiskAbs...)
|
||||
getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem")
|
||||
}
|
||||
@@ -149,7 +150,7 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Contex
|
||||
}
|
||||
|
||||
// caller should _not hold s.mtx
|
||||
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) {
|
||||
func (s *abstractionsCache) tryLoadOnDiskAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
@@ -158,9 +159,10 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Co
|
||||
},
|
||||
JobID: nil,
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
AbstractionStepHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
@@ -175,12 +177,12 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Co
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) {
|
||||
func (s *abstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) {
|
||||
// no s.mtx, we only use the public interface in this function
|
||||
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
|
||||
obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, keep)
|
||||
obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, types, keep)
|
||||
|
||||
if check != nil {
|
||||
check(obsoleteAbs)
|
||||
@@ -193,11 +195,11 @@ func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
WithError(res.DestroyErr).
|
||||
Error("cannot destroy stale send step abstraction")
|
||||
Error("cannot destroy abstraction")
|
||||
} else {
|
||||
getLogger(ctx).
|
||||
WithField("abstraction", res.Abstraction).
|
||||
Info("destroyed stale send step abstraction")
|
||||
Info("destroyed abstraction")
|
||||
}
|
||||
}
|
||||
if hadErr {
|
||||
@@ -0,0 +1,214 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type ReplicationGuaranteeOptions struct {
|
||||
Initial ReplicationGuaranteeKind
|
||||
Incremental ReplicationGuaranteeKind
|
||||
}
|
||||
|
||||
func replicationGuaranteeOptionsFromPDU(in *pdu.ReplicationConfigProtection) (o ReplicationGuaranteeOptions, _ error) {
|
||||
if in == nil {
|
||||
return o, errors.New("pdu.ReplicationConfigProtection must not be nil")
|
||||
}
|
||||
initial, err := replicationGuaranteeKindFromPDU(in.GetInitial())
|
||||
if err != nil {
|
||||
return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Initial")
|
||||
}
|
||||
incremental, err := replicationGuaranteeKindFromPDU(in.GetIncremental())
|
||||
if err != nil {
|
||||
return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Incremental")
|
||||
}
|
||||
o = ReplicationGuaranteeOptions{
|
||||
Initial: initial,
|
||||
Incremental: incremental,
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func replicationGuaranteeKindFromPDU(in pdu.ReplicationGuaranteeKind) (k ReplicationGuaranteeKind, _ error) {
|
||||
switch in {
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeNothing:
|
||||
return ReplicationGuaranteeKindNone, nil
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication:
|
||||
return ReplicationGuaranteeKindIncremental, nil
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeResumability:
|
||||
return ReplicationGuaranteeKindResumability, nil
|
||||
|
||||
case pdu.ReplicationGuaranteeKind_GuaranteeInvalid:
|
||||
fallthrough
|
||||
default:
|
||||
return k, errors.Errorf("%q", in.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (o ReplicationGuaranteeOptions) Strategy(incremental bool) ReplicationGuaranteeStrategy {
|
||||
g := o.Initial
|
||||
if incremental {
|
||||
g = o.Incremental
|
||||
}
|
||||
return ReplicationGuaranteeFromKind(g)
|
||||
}
|
||||
|
||||
//go:generate enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind
|
||||
type ReplicationGuaranteeKind int
|
||||
|
||||
const (
|
||||
ReplicationGuaranteeKindResumability ReplicationGuaranteeKind = 1 << iota
|
||||
ReplicationGuaranteeKindIncremental
|
||||
ReplicationGuaranteeKindNone
|
||||
)
|
||||
|
||||
type ReplicationGuaranteeStrategy interface {
|
||||
Kind() ReplicationGuaranteeKind
|
||||
SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error)
|
||||
ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error)
|
||||
SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error)
|
||||
}
|
||||
|
||||
func ReplicationGuaranteeFromKind(k ReplicationGuaranteeKind) ReplicationGuaranteeStrategy {
|
||||
switch k {
|
||||
case ReplicationGuaranteeKindNone:
|
||||
return ReplicationGuaranteeNone{}
|
||||
case ReplicationGuaranteeKindIncremental:
|
||||
return ReplicationGuaranteeIncremental{}
|
||||
case ReplicationGuaranteeKindResumability:
|
||||
return ReplicationGuaranteeResumability{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unreachable: %q %T", k, k))
|
||||
}
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeNone struct{}
|
||||
|
||||
func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind {
|
||||
return ReplicationGuaranteeKindNone
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeNone) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeNone) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeNone) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeIncremental struct{}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind {
|
||||
return ReplicationGuaranteeKindIncremental
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
|
||||
if sendArgs.FromVersion != nil {
|
||||
from, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, jid)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
getLogger(ctx).WithField("replication_guarantee", g).
|
||||
WithField("bookmark", sendArgs.From.FullPath(sendArgs.FS)).
|
||||
Info("bookmark cloning is not supported, speculating that `from` will not be destroyed until step is done")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
keep = append(keep, from)
|
||||
}
|
||||
to, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, sendArgs.ToVersion, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keep = append(keep, to)
|
||||
|
||||
return keep, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return receiverPostRecvCommon(ctx, jid, fs, toRecvd)
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeIncremental) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return senderPostRecvConfirmedCommon(ctx, jid, fs, to)
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeResumability struct{}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind {
|
||||
return ReplicationGuaranteeKindResumability
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) {
|
||||
// try to hold the FromVersion
|
||||
if sendArgs.FromVersion != nil {
|
||||
if sendArgs.FromVersion.Type == zfs.Bookmark {
|
||||
getLogger(ctx).WithField("replication_guarantee", g).WithField("fromVersion", sendArgs.FromVersion.FullPath(sendArgs.FS)).
|
||||
Debug("cannot hold a bookmark, speculating that `from` will not be destroyed until step is done")
|
||||
} else {
|
||||
from, err := HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keep = append(keep, from)
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
to, err := HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keep = append(keep, to)
|
||||
|
||||
return keep, nil
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return receiverPostRecvCommon(ctx, jid, fs, toRecvd)
|
||||
}
|
||||
|
||||
func (g ReplicationGuaranteeResumability) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
return senderPostRecvConfirmedCommon(ctx, jid, fs, to)
|
||||
}
|
||||
|
||||
// helper function used by multiple strategies
|
||||
func senderPostRecvConfirmedCommon(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
|
||||
log := getLogger(ctx).WithField("toVersion", to.FullPath(fs))
|
||||
|
||||
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, jid)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
log.Debug("not setting replication cursor, bookmark cloning not supported")
|
||||
} else {
|
||||
msg := "cannot move replication cursor, keeping hold on `to` until successful"
|
||||
log.WithError(err).Error(msg)
|
||||
err = errors.Wrap(err, msg)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
|
||||
}
|
||||
|
||||
return []Abstraction{toReplicationCursor}, nil
|
||||
}
|
||||
|
||||
// helper function used by multiple strategies
|
||||
func receiverPostRecvCommon(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) {
|
||||
getLogger(ctx).Debug("create new last-received-hold")
|
||||
lrh, err := CreateLastReceivedHold(ctx, fs, toRecvd, jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []Abstraction{lrh}, nil
|
||||
}
|
||||
@@ -3,5 +3,5 @@ package endpoint
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
func RegisterMetrics(r prometheus.Registerer) {
|
||||
r.MustRegister(sendAbstractionsCacheMetrics.count)
|
||||
r.MustRegister(abstractionsCacheMetrics.count)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
"github.com/zrepl/zrepl/util/semaphore"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -23,19 +24,19 @@ type AbstractionType string
|
||||
// There are a lot of exhaustive switches on AbstractionType in the code base.
|
||||
// When adding a new abstraction type, make sure to search and update them!
|
||||
const (
|
||||
AbstractionStepBookmark AbstractionType = "step-bookmark"
|
||||
AbstractionStepHold AbstractionType = "step-hold"
|
||||
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
|
||||
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
|
||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||
AbstractionStepHold AbstractionType = "step-hold"
|
||||
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
|
||||
AbstractionTentativeReplicationCursorBookmark AbstractionType = "tentative-replication-cursor-bookmark-v2"
|
||||
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
|
||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||
)
|
||||
|
||||
var AbstractionTypesAll = map[AbstractionType]bool{
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
|
||||
// Implementation Note:
|
||||
@@ -80,12 +81,12 @@ func AbstractionEquals(a, b Abstraction) bool {
|
||||
|
||||
func (t AbstractionType) Validate() error {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return nil
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
@@ -185,8 +186,8 @@ type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstra
|
||||
// returns nil if the abstraction type is not bookmark-based
|
||||
func (t AbstractionType) BookmarkExtractor() BookmarkExtractor {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
return StepBookmarkExtractor
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return TentativeReplicationCursorExtractor
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return ReplicationCursorV1Extractor
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
@@ -205,7 +206,7 @@ type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag stri
|
||||
// returns nil if the abstraction type is not hold-based
|
||||
func (t AbstractionType) HoldExtractor() HoldExtractor {
|
||||
switch t {
|
||||
case AbstractionStepBookmark:
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
@@ -220,6 +221,23 @@ func (t AbstractionType) HoldExtractor() HoldExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
func (t AbstractionType) BookmarkNamer() func(fs string, guid uint64, jobId JobID) (string, error) {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return TentativeReplicationCursorBookmarkName
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
panic("shouldn't be creating new ones")
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return ReplicationCursorBookmarkName
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
type ListZFSHoldsAndBookmarksQuery struct {
|
||||
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||
// What abstraction types should match (any contained in the set)
|
||||
@@ -240,7 +258,7 @@ type ListZFSHoldsAndBookmarksQuery struct {
|
||||
|
||||
type CreateTXGRangeBound struct {
|
||||
CreateTXG uint64
|
||||
Inclusive *zfs.NilBool // must not be nil
|
||||
Inclusive *nodefault.Bool // must not be nil
|
||||
}
|
||||
|
||||
// A non-empty range of CreateTXGs
|
||||
@@ -283,7 +301,7 @@ func (q *ListZFSHoldsAndBookmarksQuery) Validate() error {
|
||||
var createTXGRangeBoundAllowCreateTXG0 = envconst.Bool("ZREPL_ENDPOINT_LIST_ABSTRACTIONS_QUERY_CREATETXG_RANGE_BOUND_ALLOW_0", false)
|
||||
|
||||
func (i *CreateTXGRangeBound) Validate() error {
|
||||
if err := i.Inclusive.Validate(); err != nil {
|
||||
if err := i.Inclusive.ValidateNoDefault(); err != nil {
|
||||
return errors.Wrap(err, "Inclusive")
|
||||
}
|
||||
if i.CreateTXG == 0 && !createTXGRangeBoundAllowCreateTXG0 {
|
||||
@@ -402,7 +420,7 @@ func (r *CreateTXGRange) String() string {
|
||||
if r.Since == nil {
|
||||
fmt.Fprintf(&buf, "~")
|
||||
} else {
|
||||
if err := r.Since.Inclusive.Validate(); err != nil {
|
||||
if err := r.Since.Inclusive.ValidateNoDefault(); err != nil {
|
||||
fmt.Fprintf(&buf, "?")
|
||||
} else if r.Since.Inclusive.B {
|
||||
fmt.Fprintf(&buf, "[")
|
||||
@@ -418,7 +436,7 @@ func (r *CreateTXGRange) String() string {
|
||||
fmt.Fprintf(&buf, "~")
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%d", r.Until.CreateTXG)
|
||||
if err := r.Until.Inclusive.Validate(); err != nil {
|
||||
if err := r.Until.Inclusive.ValidateNoDefault(); err != nil {
|
||||
fmt.Fprintf(&buf, "?")
|
||||
} else if r.Until.Inclusive.B {
|
||||
fmt.Fprintf(&buf, "]")
|
||||
@@ -562,16 +580,17 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
|
||||
|
||||
_, add, wait := trace.WithTaskGroup(ctx, "list-abstractions-impl-fs")
|
||||
defer wait()
|
||||
for i := range fss {
|
||||
for _, fs := range fss {
|
||||
fs := fs // capture by copy
|
||||
add(func(ctx context.Context) {
|
||||
g, err := sem.Acquire(ctx)
|
||||
if err != nil {
|
||||
errCb(err, fss[i], err.Error())
|
||||
errCb(err, fs, err.Error())
|
||||
return
|
||||
}
|
||||
func() {
|
||||
defer g.Release()
|
||||
listAbstractionsImplFS(ctx, fss[i], &query, emitAbstraction, errCb)
|
||||
listAbstractionsImplFS(ctx, fs, &query, emitAbstraction, errCb)
|
||||
}()
|
||||
})
|
||||
}
|
||||
@@ -697,11 +716,9 @@ func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*Staleness
|
||||
return nil, &ListStaleQueryError{errors.New("ListStale cannot have Until != nil set on query")}
|
||||
}
|
||||
|
||||
// if asking for step holds, must also as for step bookmarks (same kind of abstraction)
|
||||
// as well as replication cursor bookmarks (for firstNotStale)
|
||||
// if asking for step holds must also ask for replication cursor bookmarks (for firstNotStale)
|
||||
ifAnyThenAll := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionStepBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) {
|
||||
@@ -730,7 +747,7 @@ type fsAjobAtype struct {
|
||||
}
|
||||
|
||||
// For step holds and bookmarks, only those older than the most recent replication cursor
|
||||
// of their (filesystem,job) is considered because younger ones cannot be stale by definition
|
||||
// of their (filesystem,job) are considered because younger ones cannot be stale by definition
|
||||
// (if we destroy them, we might actually lose the hold on the `To` for an ongoing incremental replication)
|
||||
//
|
||||
// For replication cursors and last-received-holds, only the most recent one is kept.
|
||||
@@ -772,8 +789,6 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
}
|
||||
|
||||
// stepFirstNotStaleCandidate.step
|
||||
case AbstractionStepBookmark:
|
||||
fallthrough
|
||||
case AbstractionStepHold:
|
||||
if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() {
|
||||
a := a
|
||||
@@ -797,7 +812,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
for k := range by {
|
||||
l := by[k]
|
||||
|
||||
if k.Type == AbstractionStepHold || k.Type == AbstractionStepBookmark {
|
||||
if k.Type == AbstractionStepHold {
|
||||
// all older than the most recent cursor are stale, others are always live
|
||||
|
||||
// if we don't have a replication cursor yet, use untilBound = nil
|
||||
@@ -815,14 +830,14 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
||||
untilBound = &CreateTXGRangeBound{
|
||||
CreateTXG: (*sfnsc.cursor).GetCreateTXG(),
|
||||
// if we have a cursor, can throw away step hold on both From and To
|
||||
Inclusive: &zfs.NilBool{B: true},
|
||||
Inclusive: &nodefault.Bool{B: true},
|
||||
}
|
||||
} else if sfnsc.step != nil {
|
||||
untilBound = &CreateTXGRangeBound{
|
||||
CreateTXG: (*sfnsc.step).GetCreateTXG(),
|
||||
// if we don't have a cursor, the step most recent step hold is our
|
||||
// initial replication cursor and it's possibly still live (interrupted initial replication)
|
||||
Inclusive: &zfs.NilBool{B: false},
|
||||
Inclusive: &nodefault.Bool{B: false},
|
||||
}
|
||||
} else {
|
||||
untilBound = nil // consider everything stale
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
const (
|
||||
LastReceivedHoldTagNamePrefix = "zrepl_last_received_J_"
|
||||
)
|
||||
|
||||
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
|
||||
|
||||
var _ HoldExtractor = LastReceivedHoldExtractor
|
||||
|
||||
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
var err error
|
||||
|
||||
if v.Type != zfs.Snapshot {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
jobID, err := ParseLastReceivedHoldTag(holdTag)
|
||||
if err == nil {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
Tag: holdTag,
|
||||
JobID: jobID,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
|
||||
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
|
||||
}
|
||||
jobId, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
|
||||
}
|
||||
return jobId, nil
|
||||
}
|
||||
|
||||
func LastReceivedHoldTag(jobID JobID) (string, error) {
|
||||
return lastReceivedHoldImpl(jobID.String())
|
||||
}
|
||||
|
||||
func lastReceivedHoldImpl(jobid string) (string, error) {
|
||||
tag := fmt.Sprintf("%s%s", LastReceivedHoldTagNamePrefix, jobid)
|
||||
if err := zfs.ValidHoldTag(tag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
|
||||
if !to.IsSnapshot() {
|
||||
return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
|
||||
}
|
||||
|
||||
tag, err := LastReceivedHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold tag")
|
||||
}
|
||||
|
||||
// we never want to be without a hold
|
||||
// => hold new one before releasing old hold
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, to, tag)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold newly received")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
FS: fs,
|
||||
FilesystemVersion: to,
|
||||
JobID: jobID,
|
||||
Tag: tag,
|
||||
}, nil
|
||||
}
|
||||
+52
-140
@@ -4,12 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/util/errorarray"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
@@ -53,6 +51,28 @@ func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error)
|
||||
return guid, jobID, err
|
||||
}
|
||||
|
||||
const tentativeReplicationCursorBookmarkNamePrefix = "zrepl_CURSORTENTATIVE_"
|
||||
|
||||
// v must be validated by caller
|
||||
func TentativeReplicationCursorBookmarkName(fs string, guid uint64, id JobID) (string, error) {
|
||||
return tentativeReplicationCursorBookmarkNameImpl(fs, guid, id.String())
|
||||
}
|
||||
|
||||
func tentativeReplicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
|
||||
return makeJobAndGuidBookmarkName(tentativeReplicationCursorBookmarkNamePrefix, fs, guid, jobid)
|
||||
}
|
||||
|
||||
// name is the full bookmark name, including dataset path
|
||||
//
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseTentativeReplicationCursorBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
|
||||
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, tentativeReplicationCursorBookmarkNamePrefix)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
|
||||
}
|
||||
return guid, jobID, err
|
||||
}
|
||||
|
||||
// may return nil for both values, indicating there is no cursor
|
||||
func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) {
|
||||
fsp, err := zfs.NewDatasetPath(fs)
|
||||
@@ -122,23 +142,23 @@ func GetReplicationCursors(ctx context.Context, dp *zfs.DatasetPath, jobID JobID
|
||||
//
|
||||
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
|
||||
func CreateReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
|
||||
return createBookmarkAbstraction(ctx, AbstractionReplicationCursorBookmarkV2, fs, target, jobID)
|
||||
}
|
||||
|
||||
bookmarkname, err := ReplicationCursorBookmarkName(fs, target.GetGuid(), jobID)
|
||||
func CreateTentativeReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
|
||||
return createBookmarkAbstraction(ctx, AbstractionTentativeReplicationCursorBookmark, fs, target, jobID)
|
||||
}
|
||||
|
||||
func createBookmarkAbstraction(ctx context.Context, abstractionType AbstractionType, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
|
||||
|
||||
bookmarkNamer := abstractionType.BookmarkNamer()
|
||||
if bookmarkNamer == nil {
|
||||
panic(abstractionType)
|
||||
}
|
||||
|
||||
bookmarkname, err := bookmarkNamer(fs, target.GetGuid(), jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "determine replication cursor name")
|
||||
}
|
||||
|
||||
if target.IsBookmark() && target.GetName() == bookmarkname {
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: AbstractionReplicationCursorBookmarkV2,
|
||||
FS: fs,
|
||||
FilesystemVersion: target,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !target.IsSnapshot() {
|
||||
return nil, zfs.ErrBookmarkCloningNotSupported
|
||||
return nil, errors.Wrapf(err, "determine %s name", abstractionType)
|
||||
}
|
||||
|
||||
// idempotently create bookmark (guid is encoded in it)
|
||||
@@ -152,125 +172,13 @@ func CreateReplicationCursor(ctx context.Context, fs string, target zfs.Filesyst
|
||||
}
|
||||
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: AbstractionReplicationCursorBookmarkV2,
|
||||
Type: abstractionType,
|
||||
FS: fs,
|
||||
FilesystemVersion: cursorBookmark,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
ReplicationCursorBookmarkNamePrefix = "zrepl_last_received_J_"
|
||||
)
|
||||
|
||||
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
|
||||
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
|
||||
}
|
||||
jobId, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
|
||||
}
|
||||
return jobId, nil
|
||||
}
|
||||
|
||||
func LastReceivedHoldTag(jobID JobID) (string, error) {
|
||||
return lastReceivedHoldImpl(jobID.String())
|
||||
}
|
||||
|
||||
func lastReceivedHoldImpl(jobid string) (string, error) {
|
||||
tag := fmt.Sprintf("%s%s", ReplicationCursorBookmarkNamePrefix, jobid)
|
||||
if err := zfs.ValidHoldTag(tag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
|
||||
if !to.IsSnapshot() {
|
||||
return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
|
||||
}
|
||||
|
||||
tag, err := LastReceivedHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold tag")
|
||||
}
|
||||
|
||||
// we never want to be without a hold
|
||||
// => hold new one before releasing old hold
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, to, tag)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "last-received-hold: hold newly received")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
FS: fs,
|
||||
FilesystemVersion: to,
|
||||
JobID: jobID,
|
||||
Tag: tag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func MoveLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) error {
|
||||
|
||||
_, err := CreateLastReceivedHold(ctx, fs, to, jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q := ListZFSHoldsAndBookmarksQuery{
|
||||
What: AbstractionTypeSet{
|
||||
AbstractionLastReceivedHold: true,
|
||||
},
|
||||
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||
FS: &fs,
|
||||
},
|
||||
JobID: &jobID,
|
||||
CreateTXG: CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: &CreateTXGRangeBound{
|
||||
CreateTXG: to.GetCreateTXG(),
|
||||
Inclusive: &zfs.NilBool{B: false},
|
||||
},
|
||||
},
|
||||
Concurrency: 1,
|
||||
}
|
||||
abs, absErrs, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "last-received-hold: list")
|
||||
}
|
||||
if len(absErrs) > 0 {
|
||||
return errors.Wrap(ListAbstractionsErrors(absErrs), "last-received-hold: list")
|
||||
}
|
||||
|
||||
getLogger(ctx).WithField("last-received-holds", fmt.Sprintf("%s", abs)).Debug("releasing last-received-holds")
|
||||
|
||||
var errs []error
|
||||
for res := range BatchDestroy(ctx, abs) {
|
||||
log := getLogger(ctx).
|
||||
WithField("last-received-hold", res.Abstraction)
|
||||
if res.DestroyErr != nil {
|
||||
errs = append(errs, res.DestroyErr)
|
||||
log.WithError(err).
|
||||
Error("cannot release last-received-hold")
|
||||
} else {
|
||||
log.Info("released last-received-hold")
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return errorarray.Wrap(errs, "last-received-hold: release")
|
||||
}
|
||||
}
|
||||
|
||||
func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
panic("impl error")
|
||||
@@ -308,24 +216,28 @@ func ReplicationCursorV1Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ HoldExtractor = LastReceivedHoldExtractor
|
||||
var _ BookmarkExtractor = TentativeReplicationCursorExtractor
|
||||
|
||||
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
var err error
|
||||
|
||||
if v.Type != zfs.Snapshot {
|
||||
func TentativeReplicationCursorExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
jobID, err := ParseLastReceivedHoldTag(holdTag)
|
||||
fullname := v.ToAbsPath(fs)
|
||||
|
||||
guid, jobid, err := ParseTentativeReplicationCursorBookmarkName(fullname)
|
||||
if guid != v.Guid {
|
||||
// TODO log this possibly tinkered-with bookmark
|
||||
return nil
|
||||
}
|
||||
if err == nil {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionLastReceivedHold,
|
||||
bm := &bookmarkBasedAbstraction{
|
||||
Type: AbstractionTentativeReplicationCursorBookmark,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
Tag: holdTag,
|
||||
JobID: jobID,
|
||||
JobID: jobid,
|
||||
}
|
||||
return bm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
|
||||
|
||||
func StepHoldTag(jobid JobID) (string, error) {
|
||||
return stepHoldTagImpl(jobid.String())
|
||||
}
|
||||
|
||||
func stepHoldTagImpl(jobid string) (string, error) {
|
||||
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
|
||||
if err := zfs.ValidHoldTag(t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseStepHoldTag(tag string) (JobID, error) {
|
||||
match := stepHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
|
||||
}
|
||||
jobID, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
const stepBookmarkNamePrefix = "zrepl_STEP"
|
||||
|
||||
// v must be validated by caller
|
||||
func StepBookmarkName(fs string, guid uint64, id JobID) (string, error) {
|
||||
return stepBookmarkNameImpl(fs, guid, id.String())
|
||||
}
|
||||
|
||||
func stepBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
|
||||
return makeJobAndGuidBookmarkName(stepBookmarkNamePrefix, fs, guid, jobid)
|
||||
}
|
||||
|
||||
// name is the full bookmark name, including dataset path
|
||||
//
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseStepBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
|
||||
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, stepBookmarkNamePrefix)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
|
||||
}
|
||||
return guid, jobID, err
|
||||
}
|
||||
|
||||
// idempotently hold / step-bookmark `version`
|
||||
//
|
||||
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
|
||||
func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
if v.IsSnapshot() {
|
||||
|
||||
tag, err := StepHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "step hold tag")
|
||||
}
|
||||
|
||||
if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil {
|
||||
return nil, errors.Wrap(err, "step hold: zfs")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs,
|
||||
Tag: tag,
|
||||
JobID: jobID,
|
||||
FilesystemVersion: v,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !v.IsBookmark() {
|
||||
panic(fmt.Sprintf("version must bei either snapshot or bookmark, got %#v", v))
|
||||
}
|
||||
|
||||
bmname, err := StepBookmarkName(fs, v.Guid, jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create step bookmark: determine bookmark name")
|
||||
}
|
||||
// idempotently create bookmark
|
||||
stepBookmark, err := zfs.ZFSBookmark(ctx, fs, v, bmname)
|
||||
if err != nil {
|
||||
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||
// TODO we could actually try to find a local snapshot that has the requested GUID
|
||||
// however, the replication algorithm prefers snapshots anyways, so this quest
|
||||
// is most likely not going to be successful. Also, there's the possibility that
|
||||
// the caller might want to filter what snapshots are eligibile, and this would
|
||||
// complicate things even further.
|
||||
return nil, err // TODO go1.13 use wrapping
|
||||
}
|
||||
return nil, errors.Wrap(err, "create step bookmark: zfs")
|
||||
}
|
||||
return &bookmarkBasedAbstraction{
|
||||
Type: AbstractionStepBookmark,
|
||||
FS: fs,
|
||||
FilesystemVersion: stepBookmark,
|
||||
JobID: jobID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ BookmarkExtractor = StepBookmarkExtractor
|
||||
|
||||
func StepBookmarkExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
|
||||
if v.Type != zfs.Bookmark {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
fullname := v.ToAbsPath(fs)
|
||||
|
||||
guid, jobid, err := ParseStepBookmarkName(fullname)
|
||||
if guid != v.Guid {
|
||||
// TODO log this possibly tinkered-with bookmark
|
||||
return nil
|
||||
}
|
||||
if err == nil {
|
||||
bm := &bookmarkBasedAbstraction{
|
||||
Type: AbstractionStepBookmark,
|
||||
FS: fs.ToString(),
|
||||
FilesystemVersion: v,
|
||||
JobID: jobid,
|
||||
}
|
||||
return bm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ HoldExtractor = StepHoldExtractor
|
||||
|
||||
func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
if v.Type != zfs.Snapshot {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
jobID, err := ParseStepHoldTag(holdTag)
|
||||
if err == nil {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs.ToString(),
|
||||
Tag: holdTag,
|
||||
FilesystemVersion: v,
|
||||
JobID: jobID,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
|
||||
|
||||
func StepHoldTag(jobid JobID) (string, error) {
|
||||
return stepHoldTagImpl(jobid.String())
|
||||
}
|
||||
|
||||
func stepHoldTagImpl(jobid string) (string, error) {
|
||||
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
|
||||
if err := zfs.ValidHoldTag(t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// err != nil always means that the bookmark is not a step bookmark
|
||||
func ParseStepHoldTag(tag string) (JobID, error) {
|
||||
match := stepHoldTagRE.FindStringSubmatch(tag)
|
||||
if match == nil {
|
||||
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
|
||||
}
|
||||
jobID, err := MakeJobID(match[1])
|
||||
if err != nil {
|
||||
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
// idempotently hold `version`
|
||||
func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
|
||||
if !v.IsSnapshot() {
|
||||
panic(fmt.Sprintf("version must be a snapshot got %#v", v))
|
||||
}
|
||||
|
||||
tag, err := StepHoldTag(jobID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "step hold tag")
|
||||
}
|
||||
|
||||
if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil {
|
||||
return nil, errors.Wrap(err, "step hold: zfs")
|
||||
}
|
||||
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs,
|
||||
Tag: tag,
|
||||
JobID: jobID,
|
||||
FilesystemVersion: v,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
var _ HoldExtractor = StepHoldExtractor
|
||||
|
||||
func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
|
||||
if v.Type != zfs.Snapshot {
|
||||
panic("impl error")
|
||||
}
|
||||
|
||||
jobID, err := ParseStepHoldTag(holdTag)
|
||||
if err == nil {
|
||||
return &holdBasedAbstraction{
|
||||
Type: AbstractionStepHold,
|
||||
FS: fs.ToString(),
|
||||
Tag: holdTag,
|
||||
FilesystemVersion: v,
|
||||
JobID: jobID,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
)
|
||||
|
||||
func TestCreateTXGRange(t *testing.T) {
|
||||
@@ -47,8 +47,8 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "wrong order obvious",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{23, &zfs.NilBool{B: true}},
|
||||
Until: &CreateTXGRangeBound{20, &zfs.NilBool{B: true}},
|
||||
Since: &CreateTXGRangeBound{23, &nodefault.Bool{B: true}},
|
||||
Until: &CreateTXGRangeBound{20, &nodefault.Bool{B: true}},
|
||||
},
|
||||
expectString: "[23,20]",
|
||||
},
|
||||
@@ -56,8 +56,8 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "wrong order edge-case could also be empty",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{23, &zfs.NilBool{B: false}},
|
||||
Until: &CreateTXGRangeBound{22, &zfs.NilBool{B: true}},
|
||||
Since: &CreateTXGRangeBound{23, &nodefault.Bool{B: false}},
|
||||
Until: &CreateTXGRangeBound{22, &nodefault.Bool{B: true}},
|
||||
},
|
||||
expectString: "(23,22]",
|
||||
},
|
||||
@@ -65,8 +65,8 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "empty",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
Until: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
Since: &CreateTXGRangeBound{2, &nodefault.Bool{B: false}},
|
||||
Until: &CreateTXGRangeBound{2, &nodefault.Bool{B: false}},
|
||||
},
|
||||
expectString: "(2,2)",
|
||||
},
|
||||
@@ -74,8 +74,8 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "inclusive-since-exclusive-until",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: true}},
|
||||
Until: &CreateTXGRangeBound{5, &zfs.NilBool{B: false}},
|
||||
Since: &CreateTXGRangeBound{2, &nodefault.Bool{B: true}},
|
||||
Until: &CreateTXGRangeBound{5, &nodefault.Bool{B: false}},
|
||||
},
|
||||
expectString: "[2,5)",
|
||||
expect: []testCaseExpectation{
|
||||
@@ -92,8 +92,8 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "exclusive-since-inclusive-until",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
Until: &CreateTXGRangeBound{5, &zfs.NilBool{B: true}},
|
||||
Since: &CreateTXGRangeBound{2, &nodefault.Bool{B: false}},
|
||||
Until: &CreateTXGRangeBound{5, &nodefault.Bool{B: true}},
|
||||
},
|
||||
expectString: "(2,5]",
|
||||
expect: []testCaseExpectation{
|
||||
@@ -111,7 +111,7 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: true}},
|
||||
Until: &CreateTXGRangeBound{0, &nodefault.Bool{B: true}},
|
||||
},
|
||||
expectString: "~,0]",
|
||||
},
|
||||
@@ -119,7 +119,7 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "half-open-no-until",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
|
||||
Since: &CreateTXGRangeBound{2, &nodefault.Bool{B: false}},
|
||||
Until: nil,
|
||||
},
|
||||
expectString: "(2,~",
|
||||
@@ -138,7 +138,7 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: nil,
|
||||
Until: &CreateTXGRangeBound{4, &zfs.NilBool{B: true}},
|
||||
Until: &CreateTXGRangeBound{4, &nodefault.Bool{B: true}},
|
||||
},
|
||||
expectString: "~,4]",
|
||||
expect: []testCaseExpectation{
|
||||
@@ -154,7 +154,7 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "edgeSince",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{math.MaxUint64, &zfs.NilBool{B: true}},
|
||||
Since: &CreateTXGRangeBound{math.MaxUint64, &nodefault.Bool{B: true}},
|
||||
Until: nil,
|
||||
},
|
||||
expectString: "[18446744073709551615,~",
|
||||
@@ -169,7 +169,7 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "edgeSinceNegative",
|
||||
expectInvalid: true,
|
||||
config: &CreateTXGRange{
|
||||
Since: &CreateTXGRangeBound{math.MaxUint64, &zfs.NilBool{B: false}},
|
||||
Since: &CreateTXGRangeBound{math.MaxUint64, &nodefault.Bool{B: false}},
|
||||
Until: nil,
|
||||
},
|
||||
expectString: "(18446744073709551615,~",
|
||||
@@ -178,7 +178,7 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
name: "edgeUntil",
|
||||
expectInvalid: false,
|
||||
config: &CreateTXGRange{
|
||||
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: true}},
|
||||
Until: &CreateTXGRangeBound{0, &nodefault.Bool{B: true}},
|
||||
},
|
||||
configAllowZeroCreateTXG: true,
|
||||
expectString: "~,0]",
|
||||
@@ -193,7 +193,7 @@ func TestCreateTXGRange(t *testing.T) {
|
||||
expectInvalid: true,
|
||||
configAllowZeroCreateTXG: true,
|
||||
config: &CreateTXGRange{
|
||||
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: false}},
|
||||
Until: &CreateTXGRangeBound{0, &nodefault.Bool{B: false}},
|
||||
},
|
||||
expectString: "~,0)",
|
||||
},
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ func MakeJobID(s string) (JobID, error) {
|
||||
return JobID{}, errors.Wrap(err, "must be usable as a dataset path component")
|
||||
}
|
||||
|
||||
if _, err := stepBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
|
||||
if _, err := tentativeReplicationCursorBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
|
||||
// note that this might still fail due to total maximum name length, but we can't enforce that
|
||||
return JobID{}, errors.Wrap(err, "must be usable for a step bookmark")
|
||||
return JobID{}, errors.Wrap(err, "must be usable for a tentative replication cursor bookmark")
|
||||
}
|
||||
|
||||
if _, err := stepHoldTagImpl(s); err != nil {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Code generated by "enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
_ReplicationGuaranteeKindName_0 = "resumabilityincremental"
|
||||
_ReplicationGuaranteeKindName_1 = "none"
|
||||
)
|
||||
|
||||
var (
|
||||
_ReplicationGuaranteeKindIndex_0 = [...]uint8{0, 12, 23}
|
||||
_ReplicationGuaranteeKindIndex_1 = [...]uint8{0, 4}
|
||||
)
|
||||
|
||||
func (i ReplicationGuaranteeKind) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 2:
|
||||
i -= 1
|
||||
return _ReplicationGuaranteeKindName_0[_ReplicationGuaranteeKindIndex_0[i]:_ReplicationGuaranteeKindIndex_0[i+1]]
|
||||
case i == 4:
|
||||
return _ReplicationGuaranteeKindName_1
|
||||
default:
|
||||
return fmt.Sprintf("ReplicationGuaranteeKind(%d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
var _ReplicationGuaranteeKindValues = []ReplicationGuaranteeKind{1, 2, 4}
|
||||
|
||||
var _ReplicationGuaranteeKindNameToValueMap = map[string]ReplicationGuaranteeKind{
|
||||
_ReplicationGuaranteeKindName_0[0:12]: 1,
|
||||
_ReplicationGuaranteeKindName_0[12:23]: 2,
|
||||
_ReplicationGuaranteeKindName_1[0:4]: 4,
|
||||
}
|
||||
|
||||
// ReplicationGuaranteeKindString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func ReplicationGuaranteeKindString(s string) (ReplicationGuaranteeKind, error) {
|
||||
if val, ok := _ReplicationGuaranteeKindNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to ReplicationGuaranteeKind values", s)
|
||||
}
|
||||
|
||||
// ReplicationGuaranteeKindValues returns all values of the enum
|
||||
func ReplicationGuaranteeKindValues() []ReplicationGuaranteeKind {
|
||||
return _ReplicationGuaranteeKindValues
|
||||
}
|
||||
|
||||
// IsAReplicationGuaranteeKind returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i ReplicationGuaranteeKind) IsAReplicationGuaranteeKind() bool {
|
||||
for _, v := range _ReplicationGuaranteeKindValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for ReplicationGuaranteeKind
|
||||
func (i ReplicationGuaranteeKind) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for ReplicationGuaranteeKind
|
||||
func (i *ReplicationGuaranteeKind) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("ReplicationGuaranteeKind should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = ReplicationGuaranteeKindString(s)
|
||||
return err
|
||||
}
|
||||
@@ -5,13 +5,19 @@ go 1.12
|
||||
require (
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/gdamore/tcell v1.2.0
|
||||
github.com/gdamore/tcell/v2 v2.2.0
|
||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909
|
||||
github.com/go-logfmt/logfmt v0.4.0
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator v9.31.0+incompatible
|
||||
github.com/go-playground/validator/v10 v10.4.1
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
||||
github.com/golang/protobuf v1.3.2
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/golang/protobuf v1.4.3
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
||||
github.com/kisielk/gotool v1.0.0 // indirect
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.8
|
||||
@@ -28,15 +34,19 @@ require (
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
||||
github.com/spf13/cobra v0.0.2
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/willf/bitset v1.1.10
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // go1.12 thinks it needs this
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
|
||||
gitlab.com/tslocum/cview v1.5.3
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135
|
||||
gonum.org/v1/gonum v0.7.0 // indirect
|
||||
google.golang.org/grpc v1.17.0
|
||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 // indirect
|
||||
google.golang.org/grpc v1.35.0
|
||||
google.golang.org/protobuf v1.25.0
|
||||
)
|
||||
|
||||
@@ -19,15 +19,21 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
|
||||
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.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=
|
||||
@@ -40,6 +46,11 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
||||
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.1.1-0.20201225194624-29bb185874fd/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909 h1:9NC8seTx6/zRmMTAdsHj/uOMi0EGHGQtjyLafBjk77Q=
|
||||
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909/go.mod h1:lP+DW8LR6Rw3ru9Vo2/y/3iiLaLWmofYql/va+7zJOk=
|
||||
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
||||
@@ -53,6 +64,15 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
|
||||
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4 h1:0suja/iKSDbEIYLbrS/8C7iArJiWpgCNcR+zwAHu7Ig=
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -88,6 +108,17 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
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/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||
@@ -114,9 +145,14 @@ github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
@@ -149,11 +185,16 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
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/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
@@ -166,6 +207,10 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
@@ -234,6 +279,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
||||
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
|
||||
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
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=
|
||||
@@ -276,6 +324,10 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0=
|
||||
github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g=
|
||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
@@ -298,28 +350,42 @@ github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f/go.mod h1:JmNwis
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
||||
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
github.com/zrepl/zrepl v0.2.0/go.mod h1:M3Zv2IGSO8iYpUjsZD6ayZ2LHy7zyMfzet9XatKOrZ8=
|
||||
gitlab.com/tslocum/cbind v0.1.4 h1:cbZXPPcieXspk8cShoT6efz7HAT8yMNQcofYWNizis4=
|
||||
gitlab.com/tslocum/cbind v0.1.4/go.mod h1:RvwYE3auSjBNlCmWeGspzn+jdLUVQ8C2QGC+0nP9ChI=
|
||||
gitlab.com/tslocum/cview v1.5.3 h1:6OTCtIUp1EkfGeLqQFRHtW8ynMJ66BhoBwuW8oZ84AQ=
|
||||
gitlab.com/tslocum/cview v1.5.3/go.mod h1:k/eLWRIF3B26VLDgtRRPkjLUXmcCsy+YCSPEAtNQgIY=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -335,6 +401,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -342,25 +409,43 @@ golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/nt
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-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-20190114222345-bf090417da8b/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-20190206041539-40960b6deb8e h1:Io7mpb+aUAGF0MKxbyQ7HQl1VgB+cL6ZJZUFaFNqVV4=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-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-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.7.0 h1:Hdks0L0hgznZLG9nzXb8vZ0rRvqNvAcgAp84y7Mwkgw=
|
||||
gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM=
|
||||
@@ -368,13 +453,37 @@ gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6d
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579 h1:Iwh0ba2kTgq2Q6mJiXhzrrjD7h11nEVnbMHFmp0/HsQ=
|
||||
google.golang.org/genproto v0.0.0-20210122163508-8081c04a3579/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
@@ -392,7 +501,11 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||
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=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
|
||||
@@ -14,32 +14,35 @@ step() {
|
||||
echo "${bold}$1${normal}"
|
||||
}
|
||||
|
||||
if ! type go >/dev/null; then
|
||||
step "go binary not installed or not in \$PATH" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$GOPATH" ]; then
|
||||
step "Make sure you have your GOPATH configured correctly" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHECKOUTPATH="${GOPATH}/src/github.com/zrepl/zrepl"
|
||||
|
||||
godep() {
|
||||
step "install build dependencies (versions pinned in build/go.mod and build/tools.go)"
|
||||
|
||||
if ! type go >/dev/null; then
|
||||
step "go binary not installed or not in \$PATH" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$GOPATH" ]; then
|
||||
step "Your GOPATH is not configured correctly" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! (echo "$PATH" | grep "${GOPATH}/bin" > /dev/null); then
|
||||
step "GOPATH/bin is not in your PATH (it should be towards the start of it)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pushd "$(dirname "${BASH_SOURCE[0]}")"/build
|
||||
set -x
|
||||
export GO111MODULE=on # otherwise, a checkout of this repo in GOPATH will disable modules on Go 1.12 and earlier
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/stringer" golang.org/x/tools/cmd/stringer
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/protoc-gen-go" github.com/golang/protobuf/protoc-gen-go
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/enumer" github.com/alvaroloes/enumer
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/goimports" golang.org/x/tools/cmd/goimports
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/golangci-lint" github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/gocovmerge" github.com/wadey/gocovmerge
|
||||
source <(go env)
|
||||
export GOOS="$GOHOSTOS"
|
||||
export GOARCH="$GOHOSTARCH"
|
||||
# TODO GOARM=$GOHOSTARM?
|
||||
cat tools.go | grep _ | awk -F'"' '{print $2}' | tee | xargs -tI '{}' go install '{}'
|
||||
set +x
|
||||
popd
|
||||
if ! type stringer || ! type protoc-gen-go || ! type enumer || ! type goimports || ! type golangci-lint || ! type gocovmerge; then
|
||||
if ! type stringer || ! type protoc-gen-go || ! type protoc-gen-go-grpc || ! type enumer || ! type goimports || ! type golangci-lint || ! type gocovmerge; then
|
||||
echo "Installed dependencies but can't find them in \$PATH, adjust it to contain \$GOPATH/bin" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -51,8 +54,9 @@ docdep() {
|
||||
exit 1
|
||||
fi
|
||||
step "Installing doc build dependencies"
|
||||
local reqpath="${CHECKOUTPATH}/docs/requirements.txt"
|
||||
if [ ! -z "$ZREPL_LAZY_DOCS_REQPATH" ]; then
|
||||
# shellcheck disable=SC2155
|
||||
local reqpath="$(dirname "${BASH_SOURCE[0]}")/docs/requirements.txt"
|
||||
if [ -n "$ZREPL_LAZY_DOCS_REQPATH" ]; then
|
||||
reqpath="$ZREPL_LAZY_DOCS_REQPATH"
|
||||
fi
|
||||
pip3 install -r "$reqpath"
|
||||
@@ -63,9 +67,15 @@ release() {
|
||||
make release
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2198
|
||||
if [ -z "$@" ]; then
|
||||
step "No command specified, exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in "$@"; do
|
||||
case "$cmd" in
|
||||
godep|docdep|release_bins|docs)
|
||||
godep|docdep|release|docs)
|
||||
eval $cmd
|
||||
continue
|
||||
;;
|
||||
|
||||
@@ -82,7 +82,7 @@ func (l Level) String() string {
|
||||
case Error:
|
||||
return "error"
|
||||
default:
|
||||
return string(l)
|
||||
return fmt.Sprintf("unknown level %d", l)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ package main
|
||||
import (
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/client"
|
||||
"github.com/zrepl/zrepl/client/status"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.AddSubcommand(daemon.DaemonCmd)
|
||||
cli.AddSubcommand(client.StatusCmd)
|
||||
cli.AddSubcommand(status.Subcommand)
|
||||
cli.AddSubcommand(client.SignalCmd)
|
||||
cli.AddSubcommand(client.StdinserverCmd)
|
||||
cli.AddSubcommand(client.ConfigcheckCmd)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM debian:latest
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
devscripts \
|
||||
dh-exec
|
||||
|
||||
RUN mkdir -p /build/src && chmod -R 0777 /build
|
||||
|
||||
WORKDIR /build/src
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
zrepl (VERSION) unstable;
|
||||
|
||||
Check https://zrepl.github.io/changelog.html.
|
||||
|
||||
-- zrepl build process <me@cschwarz.com> DATE_DASH_R_OUTPUT
|
||||
@@ -0,0 +1 @@
|
||||
11
|
||||
@@ -0,0 +1,8 @@
|
||||
Source: zrepl
|
||||
Maintainer: Christian Schwarz <me@cschwarz.com>
|
||||
Build-Depends: dh-exec
|
||||
|
||||
Package: zrepl
|
||||
Architecture: arm64 amd64 armhf i386
|
||||
Description: ZFS replication
|
||||
zrepl is a one-stop solution for ZFS filesystem replication
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_strip:
|
||||
|
||||
override_dh_auto_build:
|
||||
|
||||
override_dh_auto_clean:
|
||||
|
||||
rm -rf debian/renamedir
|
||||
|
||||
override_dh_auto_install:
|
||||
|
||||
# install the zrepl.service for
|
||||
# dh_install_systemd and dh_systemd_{start,enable}
|
||||
install dist/systemd/zrepl.service debian/zrepl.service
|
||||
sed -i 's#ExecStart=/usr/local/bin/zrepl #ExecStart=/usr/bin/zrepl #' debian/zrepl.service
|
||||
sed -i 's#ExecStartPre=/usr/local/bin/zrepl #ExecStartPre=/usr/bin/zrepl #' debian/zrepl.service
|
||||
|
||||
mkdir -p debian/renamedir
|
||||
|
||||
# install binary
|
||||
stat artifacts/$(ZREPL_DPKG_ZREPL_BINARY_FILENAME)
|
||||
cp --preserve=all artifacts/$(ZREPL_DPKG_ZREPL_BINARY_FILENAME) debian/renamedir/zrepl
|
||||
dh_install debian/renamedir/zrepl usr/bin
|
||||
|
||||
# install bash completion
|
||||
dh_install artifacts/bash_completion etc/bash_completion.d/zrepl
|
||||
dh_install artifacts/_zrepl.zsh_completion usr/share/zsh/vendor-completions
|
||||
|
||||
# install docs
|
||||
dh_install artifacts/docs/html usr/share/doc/zrepl/docs/
|
||||
|
||||
# install examples
|
||||
dh_install config/samples/* usr/share/doc/zrepl/examples
|
||||
|
||||
# install default config
|
||||
mkdir -p debian/tmp/etc/zrepl
|
||||
chmod 0700 debian/tmp/etc/zrepl
|
||||
sed 's#USR_SHARE_ZREPL#/usr/share/doc/zrepl#' packaging/systemd-default-zrepl.yml > debian/renamedir/zrepl.yml
|
||||
dh_install debian/renamedir/zrepl.yml etc/zrepl
|
||||
|
||||
# save git revision of this packaging repo
|
||||
echo $$(git rev-parse HEAD) > debian/packaging-repo-git-revision
|
||||
dh_install debian/packaging-repo-git-revision usr/share/doc/zrepl
|
||||
|
||||
override_dh_auto_test:
|
||||
# don't run tests at this point
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM fedora:latest
|
||||
|
||||
RUN dnf install -y git make bash rpm-build 'dnf-command(builddep)'
|
||||
ADD packaging/rpm/zrepl.spec /tmp/zrepl.spec
|
||||
RUN dnf builddep -y /tmp/zrepl.spec
|
||||
RUN mkdir -p /build/src && chmod -R 0777 /build
|
||||
WORKDIR /build/src
|
||||
@@ -0,0 +1,78 @@
|
||||
# This Spec file packages pre-built artifacts in the artifacts directory
|
||||
# into an RPM. This means that rpmbuild won't actually build any new artifacts
|
||||
#
|
||||
# Read the Makefile first, then come back here.
|
||||
|
||||
# Global meta data
|
||||
Version: SUBSTITUTED_BY_MAKEFILE
|
||||
%global common_description %{expand:
|
||||
zrepl is a one-stop, integrated solution for ZFS replication.}
|
||||
|
||||
# use gzip to be compatible with centos7
|
||||
%define _source_payload w9.gzdio
|
||||
%define _binary_payload w9.gzdio
|
||||
|
||||
# don't strip pre-built binaries
|
||||
%define __strip /usr/bin/true
|
||||
|
||||
Name: zrepl
|
||||
Release: 1
|
||||
Summary: One-stop, integrated solution for ZFS replication
|
||||
License: MIT
|
||||
URL: https://zrepl.github.io/
|
||||
# Source: we use rpmbuild --build-in-tree => no source
|
||||
BuildRequires: systemd
|
||||
Requires(post): systemd
|
||||
Requires(preun): systemd
|
||||
Requires(postun): systemd
|
||||
|
||||
%description
|
||||
%{common_description}
|
||||
|
||||
%prep
|
||||
# we use rpmbuild --build-in-tree => no prep or setup
|
||||
|
||||
%build
|
||||
# we don't actually need to build anything here, that has already been done by the Makefile
|
||||
|
||||
# Correct the path in the systemd unit file
|
||||
sed s:/usr/local/bin/:%{_bindir}/:g dist/systemd/zrepl.service > artifacts/rpmbuild/zrepl.service
|
||||
|
||||
# Generate the default configuration file
|
||||
sed 's#USR_SHARE_ZREPL#%{_datadir}/doc/zrepl#' packaging/systemd-default-zrepl.yml > artifacts/rpmbuild/zrepl.yml
|
||||
|
||||
%install
|
||||
install -Dm 0755 artifacts/%{_zrepl_binary_filename} %{buildroot}%{_bindir}/zrepl
|
||||
install -Dm 0644 artifacts/rpmbuild/zrepl.service %{buildroot}%{_unitdir}/zrepl.service
|
||||
install -Dm 0644 artifacts/_zrepl.zsh_completion %{buildroot}%{_datadir}/zsh/site-functions/_zrepl
|
||||
install -Dm 0644 artifacts/bash_completion %{buildroot}%{_datadir}/bash-completion/completions/zrepl
|
||||
install -Dm 0644 artifacts/rpmbuild/zrepl.yml %{buildroot}%{_sysconfdir}/zrepl/zrepl.yml
|
||||
install -d %{buildroot}%{_datadir}/doc/zrepl
|
||||
cp -a artifacts/docs/html %{buildroot}%{_datadir}/doc/zrepl/html
|
||||
cp -a config/samples %{buildroot}%{_datadir}/doc/zrepl/examples
|
||||
|
||||
%post
|
||||
%systemd_post zrepl.service
|
||||
|
||||
|
||||
%preun
|
||||
%systemd_preun zrepl.service
|
||||
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart zrepl.service
|
||||
|
||||
|
||||
%files
|
||||
%defattr(-,root,root)
|
||||
%license LICENSE
|
||||
%{_bindir}/zrepl
|
||||
%config %{_unitdir}/zrepl.service
|
||||
%dir %{_sysconfdir}/zrepl
|
||||
%config %{_sysconfdir}/zrepl/zrepl.yml
|
||||
%{_datadir}/zsh/site-functions/_zrepl
|
||||
%{_datadir}/bash-completion/completions/zrepl
|
||||
%{_datadir}/doc/zrepl
|
||||
|
||||
%changelog
|
||||
# TODO: auto-fill changelog from git? -> need same solution for debian
|
||||
@@ -0,0 +1,13 @@
|
||||
global:
|
||||
logging:
|
||||
# use syslog instead of stdout because it makes journald happy
|
||||
- type: syslog
|
||||
format: human
|
||||
level: warn
|
||||
|
||||
jobs:
|
||||
# - name: foo
|
||||
# type: bar
|
||||
|
||||
# see USR_SHARE_ZREPL/examples
|
||||
# or https://zrepl.github.io/configuration/overview.html
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
|
||||
@@ -256,14 +256,14 @@ nextLine:
|
||||
|
||||
case string(DestroyRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
return nil, &LineError{scan.Text(), "unexpected tokens at EOL"}
|
||||
}
|
||||
stmts = append(stmts, &DestroyRootOp{rootds})
|
||||
continue nextLine
|
||||
|
||||
case string(CreateRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
return nil, &LineError{scan.Text(), "unexpected tokens at EOL"}
|
||||
}
|
||||
stmts = append(stmts, &FSOp{Op: Add, Path: rootds})
|
||||
continue nextLine
|
||||
@@ -319,7 +319,7 @@ nextLine:
|
||||
}
|
||||
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
return nil, &LineError{scan.Text(), "unexpected tokens at EOL"}
|
||||
}
|
||||
}
|
||||
return stmts, nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user