diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..2fccaad8f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/issue_labeler.yml b/.github/issue_labeler.yml new file mode 100644 index 000000000..0821bc8fe --- /dev/null +++ b/.github/issue_labeler.yml @@ -0,0 +1,2 @@ +needs_triage: + - '.*' diff --git a/.github/workflows/build_binary_from_ref.yml b/.github/workflows/build_binary_from_ref.yml new file mode 100644 index 000000000..89bfb0e6c --- /dev/null +++ b/.github/workflows/build_binary_from_ref.yml @@ -0,0 +1,49 @@ +name: "Build binary from arbitratry repo / ref" +on: + workflow_dispatch: + inputs: + repository: + description: 'The receptor repository to build from.' + required: true + default: 'ansible/receptor' + ref: + description: 'The ref to build. Can be a branch or any other valid ref.' + required: true + default: 'devel' +jobs: + build: + name: build + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + + - uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + repository: ${{ github.event.inputs.repository }} + ref: ${{ github.event.inputs.ref }} + + - name: build-all target + run: make receptor + + - name: Upload binary + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: | + pip install boto3 + ansible -i localhost, -c local all -m aws_s3 \ + -a "bucket=receptor-nightlies object=refs/${{ github.event.inputs.ref }}/receptor src=./receptor mode=put" diff --git a/.github/workflows/devel_image.yml b/.github/workflows/devel_image.yml index 7c45bcb28..5b33e46ac 100644 --- a/.github/workflows/devel_image.yml +++ b/.github/workflows/devel_image.yml @@ -12,9 +12,19 @@ jobs: name: Push devel image steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install build dependencies + run: | + pip install build + - name: Build Image run: | - TAG=receptor:devel make container + make container REPO=receptor TAG=devel + + - name: Test Image + run: podman run --rm receptor:devel receptor --version - name: Push To Quay uses: redhat-actions/push-to-registry@v2.1.1 diff --git a/.github/workflows/devel_whl.yml b/.github/workflows/devel_whl.yml index 021e84c7f..598f41f9a 100644 --- a/.github/workflows/devel_whl.yml +++ b/.github/workflows/devel_whl.yml @@ -14,7 +14,11 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - + + - name: Install build dependencies + run: | + pip install build + - name: Build wheel run: | make clean receptorctl_wheel diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml new file mode 100644 index 000000000..05eb4f4a7 --- /dev/null +++ b/.github/workflows/promote.yml @@ -0,0 +1,53 @@ +--- +name: Promote Release +on: + release: + types: [published] + +jobs: + promote: + runs-on: ubuntu-latest + steps: + - name: Checkout Receptor + uses: actions/checkout@v2 + + - name: Install python + uses: actions/setup-python@v2 + + - name: Install dependencies + run: | + python3 -m pip install twine build + + - name: Set official pypi info + run: echo pypi_repo=pypi >> $GITHUB_ENV + if: ${{ github.repository_owner == 'ansible' }} + + - name: Set unofficial pypi info + run: echo pypi_repo=testpypi >> $GITHUB_ENV + if: ${{ github.repository_owner != 'ansible' }} + + - name: Build receptorctl and upload to pypi + run: | + make receptorctl_wheel receptorctl_sdist VERSION=${{ github.event.release.tag_name }} + twine upload \ + -r ${{ env.pypi_repo }} \ + -u ${{ secrets.PYPI_USERNAME }} \ + -p ${{ secrets.PYPI_PASSWORD }} \ + receptorctl/dist/* + + - name: Log in to GHCR + run: | + echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Log in to Quay + run: | + echo ${{ secrets.QUAY_TOKEN }} | docker login quay.io -u ${{ secrets.QUAY_USER }} --password-stdin + + - name: Re-tag and promote awx image + run: | + docker pull ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} + docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:${{ github.event.release.tag_name }} + docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest + docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }} + docker push quay.io/${{ github.repository }}:latest + diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8f9e95229..7c0be5055 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,51 +3,53 @@ name: CI on: pull_request: - push: jobs: - lint: - name: lint + lint-receptor: + name: lint-receptor runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: golangci-lint uses: golangci/golangci-lint-action@v2 - with: - version: v1.39 - build: - name: build + lint-receptorctl: + name: lint-receptorctl runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v2 + - name: Checkout + uses: actions/checkout@v2 with: - go-version: 1.15 + fetch-depth: 0 - - uses: actions/cache@v2 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + - name: Setup up python + uses: actions/setup-python@v2 - - name: Checkout - uses: actions/checkout@v2 + - name: Install tox + run: pip install tox - - name: build-all target - run: make build-all - test: - name: test + - name: Run receptorctl linters + run: make receptorctl-lint + receptor: + name: receptor (Go ${{ matrix.go-version }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go-version: [1.16, 1.17] steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: ${{ matrix.go-version }} - uses: actions/cache@v2 with: @@ -58,17 +60,10 @@ jobs: restore-keys: | ${{ runner.os }}-go- - - name: Setup up python - uses: actions/setup-python@v2 - - - name: Install python packages - run: sudo apt-get install python3-wheel python3-virtualenv - - - name: Checkout - uses: actions/checkout@v2 - - - name: install receptor - run: go build -o $GOROOT/bin/receptor ./cmd/receptor-cl + - name: build and install receptor + run: | + make build-all + sudo cp ./receptor /usr/local/bin/receptor - name: Download kind binary run: curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 && chmod +x ./kind @@ -79,23 +74,107 @@ jobs: - name: Interact with the cluster run: kubectl get nodes - - name: test target + - name: Run receptor tests run: make test - - name: receptorctl-tests target - run: make receptorctl-tests - - - name: Remove sockets for artifacting - if: ${{ failure() }} - run: "find /tmp/receptor-testing -type s -exec /bin/rm {} \\;" - - name: get k8s logs if: ${{ failure() }} run: .github/workflows/artifact-k8s-logs.sh + - name: remove sockets before archiving logs + if: ${{ failure() }} + run: find /tmp/receptor-testing -name controlsock -delete + - name: Artifact receptor data uses: actions/upload-artifact@v2 if: ${{ failure() }} with: name: test-logs path: /tmp/receptor-testing + + - name: Archive receptor binary + uses: actions/upload-artifact@v2 + with: + name: receptor + path: /usr/local/bin/receptor + + receptorctl: + name: receptorctl (Python ${{ matrix.python-version }}) + needs: receptor + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, 3.9] + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Download receptor + uses: actions/download-artifact@v2 + with: + name: receptor + path: /usr/local/bin/ + + - name: Fix permissions on receptor binary + run: sudo chmod a+x /usr/local/bin/receptor + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox + run: pip install tox + + - name: Run receptorctl tests + run: make receptorctl-test + + container: + name: container + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + + - name: Install python dependencies + run: pip install build + + - name: Build container + run: make container REPO=receptor LATEST=yes + + - name: Write out basic config + run: | + cat << EOF > test.cfg + --- + - local-only: + + - control-service: + service: control + filename: /tmp/receptor.sock + + - work-command: + worktype: cat + command: cat + EOF + + - name: Run receptor (and wait a few seconds for it to boot) + run: | + podman run --name receptor -d -v $PWD/test.cfg:/etc/receptor/receptor.conf:Z localhost/receptor + sleep 3 + podman logs receptor + + - name: Submit work and assert the output we expect + run: | + output=$(podman exec -i receptor receptorctl work submit cat -l 'hello world' -f) + echo $output + if [[ "$output" != "hello world" ]]; then + echo "Output did not contain expected value" + exit 1 + fi diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml new file mode 100644 index 000000000..6c490b99e --- /dev/null +++ b/.github/workflows/stage.yml @@ -0,0 +1,72 @@ +--- +name: Stage Release +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release. (x.y.z) Will create a tag / draft release.' + required: true + default: '' + ref: + description: 'The ref to tag. Can be a branch name / SHA / etc.' + required: true + default: '' + confirm: + description: 'Are you sure? Set this to yes.' + required: true + default: 'no' +jobs: + stage: + runs-on: ubuntu-latest + permissions: + packages: write + contents: write + steps: + - name: Verify inputs + run: | + set -e + + if [[ ${{ github.event.inputs.confirm }} != "yes" ]]; then + >&2 echo "Confirm must be 'yes'" + exit 1 + fi + + if [[ ${{ github.event.inputs.version }} == "" ]]; then + >&2 echo "Set version to continue." + exit 1 + fi + + exit 0 + + - name: Checkout receptor + uses: actions/checkout@v2 + with: + ref: ${{ github.event.inputs.ref }} + + - name: Install python + uses: actions/setup-python@v2 + + - name: Install dependencies + run: | + python3 -m pip install build + + - name: Log in to registry + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build container image + run: | + make container CONTAINERCMD=docker REPO=ghcr.io/${{ github.repository_owner }}/receptor VERSION=v${{ github.event.inputs.version }} LATEST=yes + + - name: Stage container image + run: | + docker push ghcr.io/${{ github.repository_owner }}/receptor:v${{ github.event.inputs.version }} + docker push ghcr.io/${{ github.repository_owner }}/receptor:latest + + - name: Create draft release + run: | + ansible-playbook tools/ansible/stage.yml \ + -e version=${{ github.event.inputs.version }} \ + -e repo=${{ github.repository_owner }}/receptor \ + -e github_token=${{ secrets.GITHUB_TOKEN }} \ + -e target_commitish=${{ github.event.inputs.ref }} diff --git a/.github/workflows/triage_new.yml b/.github/workflows/triage_new.yml new file mode 100644 index 000000000..f032d1930 --- /dev/null +++ b/.github/workflows/triage_new.yml @@ -0,0 +1,21 @@ +name: Triage + +on: + issues: + types: + - opened + +jobs: + triage: + runs-on: ubuntu-latest + name: Label + + steps: + - name: Label issues + uses: github/issue-labeler@v2.4.1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + not-before: 2021-12-07T07:00:00Z + configuration-path: .github/issue_labeler.yml + enable-versioned-regex: 0 + if: github.event_name == 'issues' diff --git a/.gitignore b/.gitignore index 5f0351189..7f272c374 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,12 @@ receptorctl-test-venv/ .container-flag* .VERSION kubectl +/receptorctl/.VERSION /receptorctl/AUTHORS /receptorctl/ChangeLog +/receptor-python-worker/.VERSION /receptor-python-worker/ChangeLog /receptor-python-worker/AUTHORS +/receptorctl/venv/ .vagrant/ /docs/build diff --git a/.golangci.yml b/.golangci.yml index d568777ac..2382dc44d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,6 @@ +run: + timeout: 10m + linters: disable-all: true enable: @@ -66,6 +69,9 @@ linters: # - wrapcheck # TODO: Errors passed upwards should be wrapped. issues: + # Dont commit the following line. + # It will make CI pass without telling you about errors. + # fix: true exclude: - "lostcancel" # TODO: Context is not canceled on multiple occasions. Needs more detailed work to be fixed. - "SA2002|thelper|testinggoroutine" # TODO: Test interface used outside of its routine, tests need to be rewritten. diff --git a/Makefile b/Makefile index 618db45ce..bdb1581b7 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,12 @@ -# Calculate version number -# - If we are on an exact Git tag, then this is official and gets a -1 release -# - If we are not, then this is unofficial and gets a -0.date.gitref release +# If the current commit has been tagged, use that as the version. +# Otherwise include short commit hash. OFFICIAL_VERSION := $(shell if VER=`git describe --exact-match --tags 2>/dev/null`; then echo $$VER; else echo ""; fi) -VERSION := $(shell cd receptorctl && python3 setup.py --version) ifeq ($(OFFICIAL_VERSION),) -RELEASE := 0.git$(shell date +'%Y%m%d').$(shell git rev-parse --short HEAD) -OFFICIAL := -APPVER := $(VERSION)-$(RELEASE) +VERSION := $(shell git describe --tags | cut -d - -f -1)+g$(shell git rev-parse --short HEAD) else -RELEASE := 1 -OFFICIAL := yes -APPVER := $(VERSION) +VERSION := $(OFFICIAL_VERSION) endif -# Container command can be docker or podman -CONTAINERCMD ?= podman -TAG ?= receptor:latest # When building Receptor, tags can be used to remove undesired # features. This is primarily used for deploying Receptor in a @@ -48,12 +39,22 @@ else TAGPARAM=--tags $(TAGS) endif +DEBUG ?= +ifeq ($(DEBUG),1) + DEBUGFLAGS=-gcflags=all="-N -l" +else + DEBUGFLAGS= +endif + receptor: $(shell find pkg -type f -name '*.go') ./cmd/receptor-cl/receptor.go - CGO_ENABLED=0 go build -o receptor -ldflags "-X 'github.com/ansible/receptor/internal/version.Version=$(APPVER)'" $(TAGPARAM) ./cmd/receptor-cl + CGO_ENABLED=0 go build -o receptor $(DEBUGFLAGS) -ldflags "-X 'github.com/ansible/receptor/internal/version.Version=$(VERSION)'" $(TAGPARAM) ./cmd/receptor-cl lint: @golint cmd/... pkg/... example/... +receptorctl-lint: receptorctl/.VERSION + @cd receptorctl && tox -e lint + format: @find cmd/ pkg/ -type f -name '*.go' -exec go fmt {} \; @@ -77,8 +78,12 @@ else TESTCMD = -run $(RUNTEST) endif -test: - @go test ./... -p 1 -parallel=16 $(TESTCMD) -count=1 +test: receptor + PATH=${PWD}:${PATH} \ + go test ./... -p 1 -parallel=16 $(TESTCMD) -count=1 -race + +receptorctl-test: receptorctl/.VERSION + @cd receptorctl && tox -e py3 testloop: receptor @i=1; while echo "------ $$i" && \ @@ -93,47 +98,53 @@ kubetest: kubectl ./kubectl get nodes version: - @echo $(APPVER) > .VERSION - @echo ".VERSION created for $(APPVER)" + @echo $(VERSION) > .VERSION + @echo ".VERSION created for $(VERSION)" + +receptorctl/.VERSION: + echo $(VERSION) > $@ -RECEPTORCTL_WHEEL = receptorctl/dist/receptorctl-$(VERSION)-py3-none-any.whl -$(RECEPTORCTL_WHEEL): receptorctl/README.md receptorctl/setup.py $(shell find receptorctl/receptorctl -type f -name '*.py') - @cd receptorctl && python3 setup.py bdist_wheel +RECEPTORCTL_WHEEL = receptorctl/dist/receptorctl-$(VERSION:v%=%)-py3-none-any.whl +$(RECEPTORCTL_WHEEL): receptorctl/README.md receptorctl/.VERSION $(shell find receptorctl/receptorctl -type f -name '*.py') + @cd receptorctl && python3 -m build --wheel receptorctl_wheel: $(RECEPTORCTL_WHEEL) -RECEPTORCTL_SDIST = receptorctl/dist/receptorctl-$(VERSION).tar.gz -$(RECEPTORCTL_SDIST): receptorctl/README.md receptorctl/setup.py $(shell find receptorctl/receptorctl -type f -name '*.py') - @cd receptorctl && python3 setup.py sdist +RECEPTORCTL_SDIST = receptorctl/dist/receptorctl-$(VERSION:v%=%).tar.gz +$(RECEPTORCTL_SDIST): receptorctl/README.md receptorctl/.VERSION $(shell find receptorctl/receptorctl -type f -name '*.py') + @cd receptorctl && python3 -m build --sdist receptorctl_sdist: $(RECEPTORCTL_SDIST) -RECEPTOR_PYTHON_WORKER_WHEEL = receptor-python-worker/dist/receptor_python_worker-$(VERSION)-py3-none-any.whl -$(RECEPTOR_PYTHON_WORKER_WHEEL): receptor-python-worker/README.md receptor-python-worker/setup.py $(shell find receptor-python-worker/receptor_python_worker -type f -name '*.py') - @cd receptor-python-worker && python3 setup.py bdist_wheel +receptor-python-worker/.VERSION: + echo $(VERSION) > $@ + +RECEPTOR_PYTHON_WORKER_WHEEL = receptor-python-worker/dist/receptor_python_worker-$(VERSION:v%=%)-py3-none-any.whl +$(RECEPTOR_PYTHON_WORKER_WHEEL): receptor-python-worker/README.md receptor-python-worker/.VERSION $(shell find receptor-python-worker/receptor_python_worker -type f -name '*.py') + @cd receptor-python-worker && python3 -m build --wheel + +# Container command can be docker or podman +CONTAINERCMD := podman + +# Repo without tag +REPO := quay.io/ansible/receptor +# TAG is VERSION with a '-' instead of a '+', to avoid invalid image reference error. +TAG := $(subst +,-,$(VERSION)) +# Set this to tag image as :latest in addition to :$(VERSION) +LATEST := container: .container-flag-$(VERSION) .container-flag-$(VERSION): $(RECEPTORCTL_WHEEL) $(RECEPTOR_PYTHON_WORKER_WHEEL) @tar --exclude-vcs-ignores -czf packaging/container/source.tar.gz . @cp $(RECEPTORCTL_WHEEL) packaging/container @cp $(RECEPTOR_PYTHON_WORKER_WHEEL) packaging/container - $(CONTAINERCMD) build packaging/container --build-arg VERSION=$(VERSION) -t $(TAG) $(if $(OFFICIAL),-t receptor:$(VERSION),) + $(CONTAINERCMD) build packaging/container --build-arg VERSION=$(VERSION:v%=%) -t $(REPO):$(TAG) $(if $(LATEST),-t $(REPO):latest,) @touch .container-flag-$(VERSION) tc-image: container @cp receptor packaging/tc-image/ @$(CONTAINERCMD) build packaging/tc-image -t receptor-tc -receptorctl-test-venv/bin/pytest: - virtualenv receptorctl-test-venv -p python3 - receptorctl-test-venv/bin/pip install -e receptorctl - receptorctl-test-venv/bin/pip install -r receptorctl/test-requirements.txt - -receptorctl-tests: receptor receptorctl-test-venv/bin/pytest - cd receptorctl && \ - PATH=$(PATH):$(PWD) \ - ../receptorctl-test-venv/bin/pytest tests/ - clean: @rm -fv receptor receptor.exe receptor.app net @rm -rfv packaging/container/RPMS/ @@ -146,4 +157,4 @@ clean: @rm -rfv receptorctl-test-venv/ @rm -fv kubectl -.PHONY: lint format fmt pre-commit build-all test clean testloop container version receptorctl-tests kubetest +.PHONY: lint format fmt pre-commit build-all test clean testloop container version receptorctl-tests kubetest receptorctl/.VERSION receptor-python-worker/.VERSION diff --git a/cmd/receptor-cb/main.go b/cmd/receptor-cb/main.go deleted file mode 100644 index 87be44859..000000000 --- a/cmd/receptor-cb/main.go +++ /dev/null @@ -1,118 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "os" - "os/signal" - "syscall" - - "github.com/ansible/receptor/internal/version" - "github.com/ansible/receptor/pkg" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// TODO: Shameless copy from signal pkg. Remove when switching to go 1.16. -type signalCtx struct { - context.Context - - cancel context.CancelFunc - signals []os.Signal - ch chan os.Signal -} - -// TODO: Shameless copy from signal pkg. Remove when switching to go 1.16. -func (c *signalCtx) stop() { - c.cancel() - signal.Stop(c.ch) -} - -// TODO: Shameless copy from signal pkg. Remove when switching to go 1.16. -func notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) { - ctx, cancel := context.WithCancel(parent) - c := &signalCtx{ - Context: ctx, - cancel: cancel, - signals: signals, - } - c.ch = make(chan os.Signal, 1) - signal.Notify(c.ch, c.signals...) - if ctx.Err() == nil { - go func() { - select { - case <-c.ch: - c.cancel() - case <-c.Done(): - } - }() - } - - return c, c.stop -} - -func Execute() { - var cfgPath string - var cfgStr string - cmd := &cobra.Command{ - Use: "receptor", - Short: "Receptor is a network mesh overlayer", - Long: "Receptor is a flexible multi-service relayer with remote " + - "execution and orchestration capabilities linking controllers with " + - "executors across a mesh of nodes.", - Version: version.Version, - RunE: func(*cobra.Command, []string) error { - var cfg pkg.Receptor - - v := viper.New() - v.AutomaticEnv() - - switch { - case cfgPath == "" && cfgStr == "": - return fmt.Errorf("specify the node configuration eith with -c or -f") - case cfgPath != "" && cfgStr != "": - return fmt.Errorf("set only one of -c and -f") - case cfgStr != "": - buf := bytes.NewBuffer([]byte(cfgStr)) - if err := v.ReadConfig(buf); err != nil { - return fmt.Errorf("given config ist invalid: %w", err) - } - case cfgPath == "-": - if err := v.ReadConfig(os.Stdin); err != nil { - return fmt.Errorf("could not read serve config file from stdin: %w", err) - } - default: - v.SetConfigFile(cfgPath) - if err := v.ReadInConfig(); err != nil { - return fmt.Errorf("could not read serve config file: %w", err) - } - } - - if err := v.UnmarshalExact(&cfg); err != nil { - return fmt.Errorf("could not unmarshal serve config file: %w", err) - } - - // TODO replace with signal.NotifyContext when switching to go 1.16. - ctx, cancel := notifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) - defer cancel() - - return cfg.Serve(ctx) - }, - } - cmd.Flags().StringVarP(&cfgPath, "configuration-file", "f", "", "Path to the configuration file containing node definition. - for stdin") - cmd.Flags().StringVarP(&cfgStr, "configuration", "c", "", "Configuration of the node directly as string") - cmd.MarkFlagRequired("configuration-file") - cmd.MarkFlagFilename("configuration-file") - - cmd.AddCommand(tlsCmd()) - - if err := cmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func main() { - Execute() -} diff --git a/cmd/receptor-cb/tls-ca.go b/cmd/receptor-cb/tls-ca.go deleted file mode 100644 index 46d24187b..000000000 --- a/cmd/receptor-cb/tls-ca.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "time" - - "github.com/ansible/receptor/pkg/certificates" - "github.com/spf13/cobra" -) - -func initCaCmd() *cobra.Command { - var cn string - var rsaBits int - var notBefore, notAfter string - var certOut, keyOut string - cmd := &cobra.Command{ - Use: "ca", - Short: "Generate a CA key and certificate", - RunE: func(cmd *cobra.Command, args []string) error { - opts := &certificates.CertOptions{ - CommonName: cn, - Bits: rsaBits, - } - - if notBefore != "" { - t, err := time.Parse(time.RFC3339, notBefore) - if err != nil { - return err - } - opts.NotBefore = t - } - if notAfter != "" { - t, err := time.Parse(time.RFC3339, notAfter) - if err != nil { - return err - } - opts.NotAfter = t - } - - return certificates.InitCA(opts, certOut, keyOut) - }, - } - - cmd.Flags().StringVar(&cn, "cn", "", "Common name to assign to the certificate") - cmd.MarkFlagRequired("cn") - - cmd.Flags().IntVar(&rsaBits, "rsa-bits", -1, "Bit length of the encryption keys of the certificate") - cmd.MarkFlagRequired("rsa-bits") - - cmd.Flags().StringVar(¬Before, "not-before", "", "Effective (NotBefore) date/time, in RFC3339 format") - cmd.Flags().StringVar(¬After, "not-after", "", "Expiration (NotAfter) date/time, in RFC3339 format") - - cmd.Flags().StringVar(&certOut, "cert-out", "", "File to save the CA certificate to") - cmd.MarkFlagRequired("cert-out") - cmd.MarkFlagFilename("cert-out") - - cmd.Flags().StringVar(&keyOut, "key-out", "", "File to save the CA private key to") - cmd.MarkFlagRequired("key-out") - cmd.MarkFlagFilename("key-out") - - return cmd -} diff --git a/cmd/receptor-cb/tls-req.go b/cmd/receptor-cb/tls-req.go deleted file mode 100644 index 071ab38c7..000000000 --- a/cmd/receptor-cb/tls-req.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "fmt" - "net" - - "github.com/ansible/receptor/pkg/certificates" - "github.com/spf13/cobra" -) - -func makeReqCmd() *cobra.Command { - var cn string - var rsaBits int - var dnsNames []string - var ipAddresses []string - var nodeIDs []string - var keyIn, keyOut, reqOut string - - cmd := &cobra.Command{ - Use: "req", - Short: "Generate a certificate sign request", - RunE: func(cmd *cobra.Command, args []string) error { - opts := &certificates.CertOptions{ - CommonName: cn, - Bits: rsaBits, - } - opts.DNSNames = dnsNames - opts.NodeIDs = nodeIDs - for _, ipstr := range ipAddresses { - ip := net.ParseIP(ipstr) - if ip == nil { - return fmt.Errorf("invalid IP address: %s", ipstr) - } - if opts.IPAddresses == nil { - opts.IPAddresses = make([]net.IP, 0) - } - opts.IPAddresses = append(opts.IPAddresses, ip) - } - - return certificates.MakeReq(opts, keyIn, keyOut, reqOut) - }, - } - - cmd.Flags().StringVar(&cn, "cn", "", "Common name to assign to the certificate") - cmd.MarkFlagRequired("cn") - - cmd.Flags().IntVar(&rsaBits, "rsa-bits", 0, "Bit length of the encryption keys of the certificate") - - cmd.Flags().StringSliceVar(&dnsNames, "dns-name", []string{}, "DNS names to add to the certificate") - - cmd.Flags().StringSliceVar(&ipAddresses, "ip-address", []string{}, "IP addresses to add to the certificate") - - cmd.Flags().StringSliceVar(&nodeIDs, "node-id", []string{}, "Receptor node IDs to add to the certificate") - - cmd.Flags().StringVar(&keyOut, "key-out", "", "File to save the private key to (new key will be generated)") - cmd.MarkFlagFilename("key-out") - - cmd.Flags().StringVar(&keyIn, "key-in", "", "Private key to use for the request") - cmd.MarkFlagFilename("key-in") - - cmd.Flags().StringVar(&reqOut, "req-out", "", "File to save the certificate request to") - cmd.MarkFlagRequired("req-out") - cmd.MarkFlagFilename("req-out") - - return cmd -} diff --git a/cmd/receptor-cb/tls-sign.go b/cmd/receptor-cb/tls-sign.go deleted file mode 100644 index 1598f03f2..000000000 --- a/cmd/receptor-cb/tls-sign.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "time" - - "github.com/ansible/receptor/pkg/certificates" - "github.com/spf13/cobra" -) - -func signReqCmd() *cobra.Command { - var notBefore, notAfter string - var caCrt, caKey, req, certOut string - var verify bool - - cmd := &cobra.Command{ - Use: "sign", - Short: "Sign a certificate", - RunE: func(cmd *cobra.Command, args []string) error { - opts := &certificates.CertOptions{} - if notBefore != "" { - t, err := time.Parse(time.RFC3339, notBefore) - if err != nil { - return err - } - opts.NotBefore = t - } - if notAfter != "" { - t, err := time.Parse(time.RFC3339, notAfter) - if err != nil { - return err - } - opts.NotAfter = t - } - - return certificates.SignReq(opts, caCrt, caKey, req, certOut, verify) - }, - } - - cmd.Flags().StringVar(¬Before, "not-before", "", "Effective (NotBefore) date/time, in RFC3339 format") - cmd.Flags().StringVar(¬After, "not-after", "", "Expiration (NotAfter) date/time, in RFC3339 format") - - cmd.Flags().StringVar(&req, "req", "", "Certificate Request PEM filename") - cmd.MarkFlagRequired("req") - cmd.MarkFlagFilename("req") - - cmd.Flags().StringVar(&caCrt, "ca-crt", "", "CA certificate PEM filename") - cmd.MarkFlagRequired("ca-cert") - cmd.MarkFlagFilename("ca-cert") - - cmd.Flags().StringVar(&caKey, "ca-key", "", "CA private key PEM filename") - cmd.MarkFlagRequired("ca-key") - cmd.MarkFlagFilename("ca-key") - - cmd.Flags().StringVar(&certOut, "cert-out", "", "File to save the signed certificate to") - cmd.MarkFlagRequired("cert-out") - cmd.MarkFlagFilename("cert-out") - - cmd.Flags().BoolVar(&verify, "verify", false, "If true, do not prompt the user for verification") - - return cmd -} diff --git a/cmd/receptor-cb/tls.go b/cmd/receptor-cb/tls.go deleted file mode 100644 index c2b5a379d..000000000 --- a/cmd/receptor-cb/tls.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import "github.com/spf13/cobra" - -func tlsCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "tls", - Short: "Helper to manage certificates for TLS", - } - cmd.AddCommand(initCaCmd()) - cmd.AddCommand(makeReqCmd()) - cmd.AddCommand(signReqCmd()) - - return cmd -} diff --git a/docs/source/index.rst b/docs/source/index.rst index 1325ad258..a02712d6a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,7 +51,7 @@ Run the following command in a terminal to start a node called `foo`, .. code:: - receptor --node id=foo --local-only -- log-level Debug + receptor --node id=foo --local-only --log-level Debug The log shows the receptor node started successfully @@ -90,13 +90,13 @@ Container image .. code:: - podman pull quay.io/project-receptor/receptor + podman pull quay.io/ansible/receptor Start a container, which automatically runs receptor with the default config located at ``/etc/receptor/receptor.conf`` .. code:: - podman run -it --rm --name receptor quay.io/project-receptor/receptor + podman run -it --rm --name receptor quay.io/ansible/receptor In another terminal, issue a basic "status" command to the running receptor process @@ -125,3 +125,4 @@ Note: the config file does not specify a node ID, so the hostname (on the contai tls firewall developer_guide + release_process diff --git a/docs/source/k8s.rst b/docs/source/k8s.rst index b697e8536..8c381edb8 100644 --- a/docs/source/k8s.rst +++ b/docs/source/k8s.rst @@ -30,6 +30,8 @@ foo.yml kubeitpod.yml +Note at this time it is necessary to have either a tcp-listener or a tcp-peer to be able to start the control service. See https://github.com/ansible/receptor/issues/518 + .. code-block:: yaml apiVersion: v1 @@ -47,6 +49,17 @@ kubeitpod.yml Note: at least one of the containers in the pod spec must be named "worker". This is the container that stdin is passed into, and that stdout is retrieved from. +First, we need the receptor control service running in order to be able to start a kubernetes work unit. + +.. code-block:: sh + + $ receptor -c foo.yml + DEBUG 2022/01/17 10:05:56 Listening on TCP [::]:2222 + INFO 2022/01/17 10:05:56 Running control service control + INFO 2022/01/17 10:05:56 Initialization complete + +Now we can submit a kubernetes work unit. + .. code-block:: yaml $ receptorctl --socket /tmp/foo.sock work submit kubeit --param secret_kube_config=@$HOME/.kube/config --param secret_kube_pod=@kubeitpod.yml --no-payload diff --git a/docs/source/release_process.rst b/docs/source/release_process.rst new file mode 100644 index 000000000..91fb63c0f --- /dev/null +++ b/docs/source/release_process.rst @@ -0,0 +1,14 @@ +Release process +=============== + +Maintainers have the ability to run the `Stage Release `_ workflow. Running this workflow will: + +- Build and push the container image to ghcr.io. This serves as a staging environment where the image can be tested. +- Create a draft release at ``_ + +After the draft release has been created, edit it and populate the description. Once you are done, click "Publish release". + +After the release is published, the `Promote Release `_ workflow will run automatically. This workflow will: + +- Publish receptorctl to PyPi. +- Pull the container image from ghcr.io, re-tag, and push to quay.io. diff --git a/docs/source/tls.rst b/docs/source/tls.rst index 137d3c521..9d91f038f 100644 --- a/docs/source/tls.rst +++ b/docs/source/tls.rst @@ -77,6 +77,42 @@ makecerts.sh The above script will create a CA, and for each node `foo` and `bar`, create a certificate request and sign it with the CA. These certs and keys can then be used to create ``tls-server`` and ``tls-client`` definitions in the receptor config files. +Pinned Certificates +^^^^^^^^^^^^^^^^^^^ + +In a case where a TLS connection is only ever going to be made between two well-known nodes, it may be preferable to +require a specific certificate rather than accepting any certificate signed by a CA. Receptor supports certificate +pinning for this purpose. Here is an example of a pinned certificate configuration: + +.. code-block:: yaml + + --- + - node: + id: foo + + - tls-server: + name: myserver + cert: /full/path/foo.crt + key: /full/path/foo.key + requireclientcert: true + clientcas: /full/path/ca.crt + pinnedclientcert: + - E6:9B:98:A7:A5:DB:17:D6:E4:2C:DE:76:45:42:A8:79:A3:0A:C5:6D:10:42:7A:6A:C4:54:57:83:F1:0F:E2:95 + + - tcp-listener: + port: 2222 + tls: myserver + +Certificate pinning is an added requirement, and does not eliminate the need to meet other stated requirements. In the above example, the client certificate must both be signed by a CA in the `ca.crt` bundle, and also have the listed fingerprint. Multiple fingerprints may be specified, in which case a certificate matching any one of them will be accepted. + +To find the fingerprint of a given certificate, use the following OpenSSL command: + +.. code:: + + openssl x509 -in my-cert.pem -noout -fingerprint -sha256 + +SHA256 and SHA512 fingerprints are supported. SHA1 fingerprints are not supported due to the insecurity of the SHA1 algorithm. + Above the mesh TLS ^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/workceptor.rst b/docs/source/workceptor.rst index be2181d8a..f7701c115 100644 --- a/docs/source/workceptor.rst +++ b/docs/source/workceptor.rst @@ -152,6 +152,47 @@ Payloads can be passed into receptor using the "--payload" option. Note: "-f" instructs receptorctl to follow the work unit immediately, i.e. stream results to stdout. One could also use "work results" to stream the results. +Runtime Parameters +^^^^^^^^^^^^^^^^^^ + +Work commands can be configured to allow parameters to be passed to commands when work is submitted: + +.. code-block:: yaml + + - work-command: + workType: listcontents + command: ls + allowruntimeparams: true + +The ``allowruntimeparams`` option will allow parameters to be passed to the work command by the +client submitting the work. The contents of a specific directory can be listed by passing the paths +to the receptor command as positional arguments immediately after the ``workType``: + +.. code:: + + receptorctl --socket /tmp/foo.sock work submit --node bar --no-payload -f listcontents /root/ /bin/ + /bin/: + bash + sh + + /root/: + helloworld.sh + +Passing options or flags to the work command needs to be done using the ``--param`` parameter to +override the ``params`` work command setting. The ``--all`` flag can be passed to the work command this way: + +.. code:: + + receptorctl --socket /tmp/foo.sock work submit --node bar --no-payload -f --param params='--all' listcontents /root/ + . + .. + .bash_logout + .bash_profile + .bashrc + .cache + helloworld.sh + + Work list ^^^^^^^^^ "work list" returns information about all work units that have ran on this receptor node. The following shows two work units, ``12L8s8h2`` and ``T0oN0CAp`` diff --git a/go.mod b/go.mod index 328d5be78..75b86af36 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,69 @@ module github.com/ansible/receptor -go 1.15 +go 1.17 require ( - github.com/creack/pty v1.1.11 + github.com/creack/pty v1.1.17 github.com/fortytw2/leaktest v1.3.0 - github.com/fsnotify/fsnotify v1.4.9 - github.com/ghjm/cmdline v0.1.0 - github.com/golang-jwt/jwt/v4 v4.0.0 + github.com/fsnotify/fsnotify v1.5.1 + github.com/ghjm/cmdline v0.1.2 + github.com/golang-jwt/jwt/v4 v4.3.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.4.2 github.com/jupp0r/go-priority-queue v0.0.0-20160601094913-ab1073853bde - github.com/lucas-clemente/quic-go v0.18.1 - github.com/minio/highwayhash v1.0.0 + github.com/lucas-clemente/quic-go v0.25.0 + github.com/minio/highwayhash v1.0.2 github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4 github.com/prep/socketpair v0.0.0-20171228153254-c2c6a7f821c2 - github.com/rogpeppe/go-internal v1.6.1 + github.com/rogpeppe/go-internal v1.8.1 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 - github.com/spf13/cobra v1.2.1 - github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.7.0 github.com/vishvananda/netlink v1.1.0 - golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 - golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect - golang.org/x/text v0.3.6 // indirect + golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.18.6 k8s.io/apimachinery v0.18.6 k8s.io/client-go v0.18.6 ) + +require ( + github.com/cheekybits/genny v1.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/googleapis/gnostic v0.1.0 // indirect + github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/imdario/mergo v0.3.5 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect + github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo v1.16.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + golang.org/x/tools v0.1.2 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/klog v1.0.0 // indirect + k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 // indirect + sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) diff --git a/go.sum b/go.sum index f7e3d0837..8ea422dea 100644 --- a/go.sum +++ b/go.sum @@ -15,11 +15,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -28,7 +23,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -57,13 +51,7 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0 github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -74,14 +62,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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= @@ -95,22 +78,19 @@ github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghjm/cmdline v0.1.0 h1:gBTvfEXjgfawQjRFM3IUS5isdf5ODlZFWMgFzWBCyVU= -github.com/ghjm/cmdline v0.1.0/go.mod h1:w+7xIuuBUPEik5PI6gho/gLu161qFYatvw6AQ7B73K4= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/ghjm/cmdline v0.1.2 h1:XhhlCLSPx4qYf+eNDNzge3lBtymw5ZbWMnJfOFbuL6k= +github.com/ghjm/cmdline v0.1.2/go.mod h1:w+7xIuuBUPEik5PI6gho/gLu161qFYatvw6AQ7B73K4= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -123,17 +103,17 @@ github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+ github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -145,8 +125,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -161,9 +141,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -175,9 +153,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -187,7 +162,6 @@ github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -195,16 +169,11 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +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/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -213,42 +182,18 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= -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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -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 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -256,48 +201,34 @@ github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMW github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jupp0r/go-priority-queue v0.0.0-20160601094913-ab1073853bde h1:+5PMaaQtDUwOcJIUlmX89P0J3iwTvErTmyn5WghzXAQ= github.com/jupp0r/go-priority-queue v0.0.0-20160601094913-ab1073853bde/go.mod h1:RDgD/dfPmIwFH0qdUOjw71HjtWg56CtyLIoHL+R1wJw= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/pty v1.1.3/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/lucas-clemente/quic-go v0.18.1 h1:DMR7guC0NtVS8zNZR3IO7NARZvZygkSC56GGtC6cyys= -github.com/lucas-clemente/quic-go v0.18.1/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg= +github.com/lucas-clemente/quic-go v0.25.0 h1:K+X9Gvd7JXsOHtU0N2icZ2Nw3rx82uBej3mP4CLgibc= +github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc= -github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs= -github.com/marten-seemann/qtls-go1-15 v0.1.0 h1:i/YPXVxz8q9umso/5y474CNcHmTpA+5DH+mFPjx6PZg= -github.com/marten-seemann/qtls-go1-15 v0.1.0/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco= +github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= +github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk= +github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8= +github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 h1:EnzzN9fPUkUck/1CuY1FlzBaIYMoiBsdwTNmNGkwUUM= +github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= -github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -308,31 +239,31 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4 h1:MfIUBZ1bz7TgvQLVa/yPJZOGeKEgs6eTKUjz3zB4B+U= github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4/go.mod h1:RMU2gJXhratVxBDTFeOdNhd540tG57lt9FIUV0YLvIQ= -github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prep/socketpair v0.0.0-20171228153254-c2c6a7f821c2 h1:vzKDZ0uNPcOdITzZT5d4Tn2YOalCMqIhYzVNq/oRjlw= github.com/prep/socketpair v0.0.0-20171228153254-c2c6a7f821c2/go.mod h1:E/IaW35yb7xPACTLciISfz5w+jqPwmnXwDdmilSl/Nc= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -340,14 +271,10 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -369,41 +296,23 @@ github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -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/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -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 v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= @@ -416,30 +325,20 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -468,8 +367,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -478,17 +375,14 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/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-20181201002055-351d144fa1fc/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= @@ -517,14 +411,9 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -532,12 +421,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= @@ -550,13 +433,10 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -591,18 +471,11 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -613,8 +486,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -632,7 +503,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/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-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= @@ -642,7 +512,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/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-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -669,13 +538,10 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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= @@ -701,12 +567,6 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -744,24 +604,12 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -777,14 +625,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 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= @@ -805,13 +645,10 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/packaging/container/Dockerfile b/packaging/container/Dockerfile index 1395e3caa..beb88e37c 100644 --- a/packaging/container/Dockerfile +++ b/packaging/container/Dockerfile @@ -1,12 +1,14 @@ -FROM centos:8 as builder +FROM quay.io/centos/centos:stream9 as builder +ARG VERSION -RUN dnf -y update && dnf install -y golang make python3 python3-pip python3-wheel +RUN dnf -y update && dnf install -y golang make python3 python3-pip git +RUN pip install wheel ADD source.tar.gz /source WORKDIR /source -RUN make +RUN make VERSION=${VERSION} -FROM centos:8 +FROM quay.io/centos/centos:stream9 ARG VERSION LABEL license="ASL2" @@ -14,22 +16,22 @@ LABEL name="receptor" LABEL vendor="ansible" LABEL version="${VERSION}" -RUN dnf -y update && \ - dnf -y install epel-release && \ - dnf -y install tini python3-click python3-pyyaml python3-dateutil python3-pip python3-wheel && \ - dnf clean all - COPY receptorctl-${VERSION}-py3-none-any.whl /tmp COPY receptor_python_worker-${VERSION}-py3-none-any.whl /tmp -RUN pip3 install /tmp/*.whl -RUN rm /tmp/*.whl - COPY receptor.conf /etc/receptor/receptor.conf + +RUN dnf -y update && \ + dnf -y install python3-pip && \ + dnf clean all && \ + pip install --no-cache-dir wheel dumb-init && \ + pip install --no-cache-dir /tmp/*.whl && \ + rm /tmp/*.whl + COPY --from=builder /source/receptor /usr/bin/receptor ENV RECEPTORCTL_SOCKET=/tmp/receptor.sock EXPOSE 7323 -ENTRYPOINT ["/usr/bin/tini", "--"] +ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] CMD ["/usr/bin/receptor", "-c", "/etc/receptor/receptor.conf"] diff --git a/pkg/backends/backends.go b/pkg/backends/backends.go deleted file mode 100644 index 1ae2bf7ad..000000000 --- a/pkg/backends/backends.go +++ /dev/null @@ -1,109 +0,0 @@ -//go:build !no_backends -// +build !no_backends - -package backends - -import ( - "errors" - "fmt" - - "github.com/ansible/receptor/pkg/netceptor" -) - -// ErrInvalidCost indicates an invalid path cost. -var ErrInvalidCost = errors.New("cost is smaller or equal 0") - -func validateDialCost(rawCost *float64) (float64, error) { - cost := 1.0 - if rawCost != nil { - cost = *rawCost - if cost <= 0.0 { - return 0, fmt.Errorf("connection cost: %w", ErrInvalidCost) - } - } - - return cost, nil -} - -func validateListenerCost(rawCost *float64, rawNodeCost map[string]float64) (float64, map[string]float64, error) { - cost := 1.0 - if rawCost != nil { - cost = *rawCost - if cost <= 0.0 { - return 0, nil, fmt.Errorf("connection cost: %w", ErrInvalidCost) - } - } - for node, cost := range rawNodeCost { - if cost <= 0.0 { - return 0, nil, fmt.Errorf("node cost for %s: %w", node, ErrInvalidCost) - } - } - - return cost, rawNodeCost, nil -} - -// Dial to other instances. -type Dial struct { - // WS dial. - WS []WSDial `mapstructure:"ws"` - // UDP dial. - UDP []UDPDial `mapstructure:"udp"` - // TCP dial. - TCP []TCPDial `mapstructure:"tcp"` -} - -// Listen for connections of other instances. -type Listen struct { - // WS listener. - WS []WSListen `mapstructure:"ws"` - // UDP listener. - UDP []UDPListen `mapstructure:"udp"` - // TCP listener. - TCP []TCPListen `mapstructure:"tcp"` -} - -// Backends is a set of backends used by a receptor instance. -type Backends struct { - // Dial to other instances. - Dial Dial `mapstructure:"dial"` - // Listen for connections of other instances. - Listen Listen `mapstructure:"listen"` -} - -// Setup attaches the defined backends to the given netceptor. -func (b Backends) Setup(nc *netceptor.Netceptor) error { - for _, c := range b.Listen.UDP { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup udp connection from connection config: %w", err) - } - } - for _, l := range b.Dial.UDP { - if err := l.setup(nc); err != nil { - return fmt.Errorf("could not setup ws listen from listener config: %w", err) - } - } - - for _, c := range b.Listen.TCP { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup tcp connection from connection config: %w", err) - } - } - for _, l := range b.Dial.TCP { - if err := l.setup(nc); err != nil { - return fmt.Errorf("could not setup tcp listen from listener config: %w", err) - } - } - - for _, c := range b.Listen.WS { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup ws connection from connection config: %w", err) - } - } - for _, l := range b.Dial.WS { - if err := l.setup(nc); err != nil { - return fmt.Errorf("could not setup ws listen from listener config: %w", err) - } - } - - return nil -} diff --git a/pkg/backends/tcp.go b/pkg/backends/tcp.go index 81cc96a97..0908d60b6 100644 --- a/pkg/backends/tcp.go +++ b/pkg/backends/tcp.go @@ -5,6 +5,7 @@ package backends import ( "context" + "crypto/tls" "fmt" "io" "net" @@ -14,7 +15,6 @@ import ( "github.com/ansible/receptor/pkg/framer" "github.com/ansible/receptor/pkg/logger" "github.com/ansible/receptor/pkg/netceptor" - "github.com/ansible/receptor/pkg/tls" "github.com/ansible/receptor/pkg/utils" "github.com/ghjm/cmdline" ) @@ -296,7 +296,7 @@ func (cfg tcpDialerCfg) Run() error { if err != nil { return err } - tlscfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLS, host, "dns") + tlscfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLS, host, netceptor.ExpectedHostnameTypeDNS) if err != nil { return err } @@ -338,81 +338,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-backends", "tcp-peer", "Make an outbound backend connection to a TCP peer", tcpDialerCfg{}, cmdline.Section(backendSection)) } - -// TCPListen for incoming connections. -type TCPListen struct { - // TLS configuration for listening. Leave empty for no TLS at all. - TLS *tls.ServerConf `mapstructure:"tls"` - // Address to listen on ("host:port" from net package). - Address string `mapstructure:"address"` - // Path cost for this connection. Defaults to 1.0, may not be <= 0.0.` - Cost *float64 `mapstructure:"cost"` - // Extra costs for specific nodes connecting. - NodeCosts map[string]float64 `mapstructure:"node-costs"` -} - -func (c TCPListen) setup(nc *netceptor.Netceptor) error { - var tlsConf *tls.Config - var err error - if c.TLS != nil { - tlsConf, err = c.TLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls config for tcp listener %s: %w", c.Address, err) - } - } - - b, err := NewTCPListener(c.Address, tlsConf) - if err != nil { - return fmt.Errorf("could not create tcp listener %s from config: %w", c.Address, err) - } - - cost, nodeCosts, err := validateListenerCost(c.Cost, c.NodeCosts) - if err != nil { - return fmt.Errorf("invalid tcp listener config for %s: %w", c.Address, err) - } - - if err := nc.AddBackend(b, netceptor.BackendConnectionCost(cost), netceptor.BackendNodeCost(nodeCosts)); err != nil { - return fmt.Errorf("error creating backend for tcp listener %s: %w", c.Address, err) - } - - return nil -} - -// TCPDial to a remote host. -type TCPDial struct { - // TLS configuration for listening. Leave empty for no TLS at all. - TLS *tls.ServerConf `mapstructure:"tls"` - // Address to connect to ("host:port" from net package). - Address string `mapstructure:"address"` - // Path cost for this connection. Defaults to 1.0, may not be <= 0.0.` - Cost *float64 `mapstructure:"cost"` - // Do not keep redialing on lost connection. - NoRedial bool `mapstructure:"no-redial"` -} - -func (c TCPDial) setup(nc *netceptor.Netceptor) error { - var tlsConf *tls.Config - var err error - if c.TLS != nil { - tlsConf, err = c.TLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls config for tcp dial %s: %w", c.Address, err) - } - } - - b, err := NewTCPDialer(c.Address, !c.NoRedial, tlsConf) - if err != nil { - return fmt.Errorf("could not create tcp dial %s from config: %w", c.Address, err) - } - - cost, err := validateDialCost(c.Cost) - if err != nil { - return fmt.Errorf("invalid cost for tcp dial %s: %w", c.Address, err) - } - - if err := nc.AddBackend(b, netceptor.BackendConnectionCost(cost), nil); err != nil { - return fmt.Errorf("error creating backend for tcp dial %s: %w", c.Address, err) - } - - return nil -} diff --git a/pkg/backends/udp.go b/pkg/backends/udp.go index 3b3ec4e72..8d2bcc3fd 100644 --- a/pkg/backends/udp.go +++ b/pkg/backends/udp.go @@ -370,59 +370,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-backends", "UDP-peer", "Make an outbound backend connection to a UDP peer", udpDialerCfg{}, cmdline.Section(backendSection)) } - -// UDPListen for incoming connections. -type UDPListen struct { - // Address to listen on ("host:port" from net package). - Address string `mapstructure:"address"` - // Path cost for this connection. Defaults to 1.0, may not be <= 0.0.` - Cost *float64 `mapstructure:"cost"` - // Extra costs for specific nodes connecting. - NodeCosts map[string]float64 `mapstructure:"node-costs"` -} - -func (c UDPListen) setup(nc *netceptor.Netceptor) error { - b, err := NewUDPListener(c.Address) - if err != nil { - return fmt.Errorf("could not create udp listener for %s from config: %w", c.Address, err) - } - - cost, nodeCosts, err := validateListenerCost(c.Cost, c.NodeCosts) - if err != nil { - return fmt.Errorf("invalid udp listener config for %s: %w", c.Address, err) - } - - if err := nc.AddBackend(b, netceptor.BackendConnectionCost(cost), netceptor.BackendNodeCost(nodeCosts)); err != nil { - return fmt.Errorf("error creating backend for udp listener %s: %w", c.Address, err) - } - - return nil -} - -// UDPDial to a remote host. -type UDPDial struct { - // Address to connect to ("host:port" from net package). - Address string `mapstructure:"address"` - // Path cost for this connection. Defaults to 1.0, may not be <= 0.0.` - Cost *float64 `mapstructure:"cost"` - // Do not keep redialing on lost connection. - NoRedial bool `mapstructure:"no-redial"` -} - -func (c UDPDial) setup(nc *netceptor.Netceptor) error { - b, err := NewUDPDialer(c.Address, !c.NoRedial) - if err != nil { - return fmt.Errorf("could not create udp connection for %s from config: %w", c.Address, err) - } - - cost, err := validateDialCost(c.Cost) - if err != nil { - return fmt.Errorf("invalid udp listener connection for %s: %w", c.Address, err) - } - - if err := nc.AddBackend(b, netceptor.BackendConnectionCost(cost), nil); err != nil { - return fmt.Errorf("error creating backend for udp connection %s: %w", c.Address, err) - } - - return nil -} diff --git a/pkg/backends/utils.go b/pkg/backends/utils.go index 321473164..835a1a241 100644 --- a/pkg/backends/utils.go +++ b/pkg/backends/utils.go @@ -18,7 +18,8 @@ type dialerFunc func(chan struct{}) (netceptor.BackendSession, error) // dialerSession is a convenience function for backends that use dial/retry logic. func dialerSession(ctx context.Context, wg *sync.WaitGroup, redial bool, redialDelay time.Duration, - df dialerFunc) (chan netceptor.BackendSession, error) { + df dialerFunc, +) (chan netceptor.BackendSession, error) { sessChan := make(chan netceptor.BackendSession) wg.Add(1) go func() { @@ -42,8 +43,6 @@ func dialerSession(ctx context.Context, wg *sync.WaitGroup, redial bool, redialD case <-closeChan: // continue case <-ctx.Done(): - _ = sess.Close() - return } } diff --git a/pkg/backends/websockets.go b/pkg/backends/websockets.go index 9b7fc2d5c..5682d7c73 100644 --- a/pkg/backends/websockets.go +++ b/pkg/backends/websockets.go @@ -5,7 +5,7 @@ package backends import ( "context" - "errors" + "crypto/tls" "fmt" "net" "net/http" @@ -16,7 +16,6 @@ import ( "github.com/ansible/receptor/pkg/logger" "github.com/ansible/receptor/pkg/netceptor" - "github.com/ansible/receptor/pkg/tls" "github.com/ghjm/cmdline" "github.com/gorilla/websocket" ) @@ -72,7 +71,7 @@ func (b *WebsocketDialer) Start(ctx context.Context, wg *sync.WaitGroup) (chan n if resp.Body.Close(); err != nil { return nil, err } - ns := newWebsocketSession(conn, closeChan) + ns := newWebsocketSession(ctx, conn, closeChan) return ns, nil }) @@ -132,7 +131,7 @@ func (b *WebsocketListener) Start(ctx context.Context, wg *sync.WaitGroup) (chan return } - ws := newWebsocketSession(conn, nil) + ws := newWebsocketSession(ctx, conn, nil) sessChan <- ws }) b.li, err = net.Listen("tcp", b.address) @@ -140,13 +139,13 @@ func (b *WebsocketListener) Start(ctx context.Context, wg *sync.WaitGroup) (chan return nil, err } wg.Add(1) + b.server = &http.Server{ + Addr: b.address, + Handler: mux, + } go func() { defer wg.Done() var err error - b.server = &http.Server{ - Addr: b.address, - Handler: mux, - } if b.tlscfg == nil { err = b.server.Serve(b.li) } else { @@ -169,6 +168,7 @@ func (b *WebsocketListener) Start(ctx context.Context, wg *sync.WaitGroup) (chan // WebsocketSession implements BackendSession for WebsocketDialer and WebsocketListener. type WebsocketSession struct { conn *websocket.Conn + context context.Context recvChan chan *recvResult closeChan chan struct{} closeChanCloser sync.Once @@ -179,9 +179,10 @@ type recvResult struct { err error } -func newWebsocketSession(conn *websocket.Conn, closeChan chan struct{}) *WebsocketSession { +func newWebsocketSession(ctx context.Context, conn *websocket.Conn, closeChan chan struct{}) *WebsocketSession { ws := &WebsocketSession{ conn: conn, + context: ctx, recvChan: make(chan *recvResult), closeChan: closeChan, closeChanCloser: sync.Once{}, @@ -195,9 +196,13 @@ func newWebsocketSession(conn *websocket.Conn, closeChan chan struct{}) *Websock func (ns *WebsocketSession) recvChannelizer() { for { _, data, err := ns.conn.ReadMessage() - ns.recvChan <- &recvResult{ + select { + case <-ns.context.Done(): + return + case ns.recvChan <- &recvResult{ data: data, err: err, + }: } if err != nil { return @@ -327,7 +332,7 @@ func (cfg websocketDialerCfg) Run() error { if u.Scheme == "wss" && tlsCfgName == "" { tlsCfgName = "default" } - tlscfg, err := netceptor.MainInstance.GetClientTLSConfig(tlsCfgName, u.Hostname(), "dns") + tlscfg, err := netceptor.MainInstance.GetClientTLSConfig(tlsCfgName, u.Hostname(), netceptor.ExpectedHostnameTypeDNS) if err != nil { return err } @@ -369,96 +374,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-backends", "ws-peer", "Connect outbound to a websocket peer", websocketDialerCfg{}, cmdline.Section(backendSection)) } - -var ErrInvalidHTTPHeader = errors.New("invalid http header") - -type WSListen struct { - // TLS configuration for listening. Leave empty for no TLS at all. - TLS *tls.ServerConf `mapstructure:"tls"` - // Address to listen on ("host:port" from net package). - Address string `mapstructure:"address"` - // Path cost for this connection. Defaults to 1.0, may not be <= 0.0.` - Cost *float64 `mapstructure:"cost"` - // Extra costs for specific nodes connecting. - NodeCosts map[string]float64 `mapstructure:"node-costs"` - // URI path to the websocket server. Default to /. - Path *string `mapstructure:"path" ` -} - -func (c WSListen) setup(nc *netceptor.Netceptor) error { - var err error - var tlsConf *tls.Config - if c.TLS != nil { - tlsConf, err = c.TLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls config for ws listener %s: %w", c.Address, err) - } - } - - b, err := NewWebsocketListener(c.Address, tlsConf) - if c.Path != nil { - b.SetPath(*c.Path) - } - if err != nil { - return fmt.Errorf("could not create ws listener for %s from config: %w", c.Address, err) - } - - cost, nodeCosts, err := validateListenerCost(c.Cost, c.NodeCosts) - if err != nil { - return fmt.Errorf("invalid ws listener config for %s: %w", c.Address, err) - } - - if err := nc.AddBackend(b, netceptor.BackendConnectionCost(cost), netceptor.BackendNodeCost(nodeCosts)); err != nil { - return fmt.Errorf("error creating backend for ws listener %s: %w", c.Address, err) - } - - return nil -} - -// WSDial to a remote host. -type WSDial struct { - // TLS configuration for listening. Leave empty for no TLS at all. - TLS *tls.ServerConf `mapstructure:"tls"` - // URL to connect to. - URL string `mapstructure:"url"` - // Path cost for this connection. Defaults to 1.0, may not be <= 0.0.` - Cost *float64 `mapstructure:"cost"` - // Do not keep redialing on lost connection. - NoRedial bool `mapstructure:"no-redial"` - // Sends extra HTTP header on initial connection. - ExtraHeader *string `mapstructure:"extra-header"` -} - -func (c WSDial) setup(nc *netceptor.Netceptor) error { - extraHeader := "" - if c.ExtraHeader != nil { - if *c.ExtraHeader == "" || !strings.Contains(*c.ExtraHeader, ":") { - return fmt.Errorf("invalid ws parameters for ws listener %s: %w", c.URL, ErrInvalidHTTPHeader) - } - extraHeader = *c.ExtraHeader - } - - var err error - var tlsConf *tls.Config - if c.TLS != nil { - tlsConf, err = c.TLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls config for ws dialer %s: %w", c.URL, err) - } - } - b, err := NewWebsocketDialer(c.URL, tlsConf, extraHeader, !c.NoRedial) - if err != nil { - return fmt.Errorf("could not create ws dialer for %s from config: %w", c.URL, err) - } - - cost, err := validateDialCost(c.Cost) - if err != nil { - return fmt.Errorf("invalid ws listener dialer for %s: %w", c.URL, err) - } - - if err := nc.AddBackend(b, netceptor.BackendConnectionCost(cost), nil); err != nil { - return fmt.Errorf("error creating backend for ws dialer %s: %w", c.URL, err) - } - - return nil -} diff --git a/pkg/certificates/cli.go b/pkg/certificates/cli.go index 2cd4de4da..d759878b4 100644 --- a/pkg/certificates/cli.go +++ b/pkg/certificates/cli.go @@ -1,3 +1,4 @@ +//go:build !no_cert_auth // +build !no_cert_auth package certificates diff --git a/pkg/controlsvc/connect.go b/pkg/controlsvc/connect.go index 6c3884880..6f99eb4d8 100644 --- a/pkg/controlsvc/connect.go +++ b/pkg/controlsvc/connect.go @@ -1,6 +1,7 @@ package controlsvc import ( + "context" "fmt" "strings" @@ -73,8 +74,8 @@ func (t *connectCommandType) InitFromJSON(config map[string]interface{}) (Contro return c, nil } -func (c *connectCommand) ControlFunc(nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { - tlscfg, err := nc.GetClientTLSConfig(c.tlsConfigName, c.targetNode, "receptor") +func (c *connectCommand) ControlFunc(ctx context.Context, nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { + tlscfg, err := nc.GetClientTLSConfig(c.tlsConfigName, c.targetNode, netceptor.ExpectedHostnameTypeReceptor) if err != nil { return nil, err } diff --git a/pkg/controlsvc/controlsvc.go b/pkg/controlsvc/controlsvc.go index c950b09dc..cea6e614f 100644 --- a/pkg/controlsvc/controlsvc.go +++ b/pkg/controlsvc/controlsvc.go @@ -5,6 +5,7 @@ package controlsvc import ( "context" + "crypto/tls" "encoding/json" "fmt" "io" @@ -17,7 +18,6 @@ import ( "github.com/ansible/receptor/pkg/logger" "github.com/ansible/receptor/pkg/netceptor" - "github.com/ansible/receptor/pkg/tls" "github.com/ansible/receptor/pkg/utils" "github.com/ghjm/cmdline" ) @@ -123,19 +123,27 @@ func (s *Server) AddControlFunc(name string, cType ControlCommandType) error { return nil } +func errorNormal(err error) bool { + return strings.HasSuffix(err.Error(), "normal close") +} + // RunControlSession runs the server protocol on the given connection. func (s *Server) RunControlSession(conn net.Conn) { - logger.Info("Client connected to control service\n") + logger.Debug("Client connected to control service %s\n", conn.RemoteAddr().String()) defer func() { - logger.Info("Client disconnected from control service\n") - err := conn.Close() - if err != nil { - logger.Error("Error closing connection: %s\n", err) + logger.Debug("Client disconnected from control service %s\n", conn.RemoteAddr().String()) + if conn != nil { + err := conn.Close() + if err != nil { + logger.Warning("Could not close connection: %s\n", err) + } } }() _, err := conn.Write([]byte(fmt.Sprintf("Receptor Control, node %s\n", s.nc.NodeID()))) if err != nil { - logger.Error("Write error in control service: %s\n", err) + if !errorNormal(err) { + logger.Error("Could not write in control service: %s\n", err) + } return } @@ -148,12 +156,14 @@ func (s *Server) RunControlSession(conn net.Conn) { for { n, err := conn.Read(buf) if err == io.EOF { - logger.Info("Control service closed\n") + logger.Debug("Control service closed\n") done = true break } else if err != nil { - logger.Error("Read error in control service: %s\n", err) + if !errorNormal(err) { + logger.Warning("Could not read in control service: %s\n", err) + } return } @@ -188,7 +198,9 @@ func (s *Server) RunControlSession(conn net.Conn) { if err != nil { _, err = conn.Write([]byte(fmt.Sprintf("ERROR: %s\n", err))) if err != nil { - logger.Error("Write error in control service: %s\n", err) + if !errorNormal(err) { + logger.Error("Write error in control service: %s\n", err) + } return } @@ -224,13 +236,19 @@ func (s *Server) RunControlSession(conn net.Conn) { cc, err = ct.InitFromJSON(jsonData) } if err == nil { - cfr, err = cc.ControlFunc(s.nc, cfo) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cfr, err = cc.ControlFunc(ctx, s.nc, cfo) } if err != nil { - logger.Error(err.Error()) + if !errorNormal(err) { + logger.Error(err.Error()) + } _, err = conn.Write([]byte(fmt.Sprintf("ERROR: %s\n", err))) if err != nil { - logger.Error("Write error in control service: %s\n", err) + if !errorNormal(err) { + logger.Error("Write error in control service: %s\n", err) + } return } @@ -239,7 +257,9 @@ func (s *Server) RunControlSession(conn net.Conn) { if err != nil { _, err = conn.Write([]byte(fmt.Sprintf("ERROR: could not convert response to JSON: %s\n", err))) if err != nil { - logger.Error("Write error in control service: %s\n", err) + if !errorNormal(err) { + logger.Error("Write error in control service: %s\n", err) + } return } @@ -247,7 +267,9 @@ func (s *Server) RunControlSession(conn net.Conn) { rbytes = append(rbytes, '\n') _, err = conn.Write(rbytes) if err != nil { - logger.Error("Write error in control service: %s\n", err) + if !errorNormal(err) { + logger.Error("Write error in control service: %s\n", err) + } return } @@ -255,7 +277,9 @@ func (s *Server) RunControlSession(conn net.Conn) { } else { _, err = conn.Write([]byte("ERROR: Unknown command\n")) if err != nil { - logger.Error("Write error in control service: %s\n", err) + if !errorNormal(err) { + logger.Error("Write error in control service: %s\n", err) + } return } @@ -265,7 +289,8 @@ func (s *Server) RunControlSession(conn net.Conn) { // RunControlSvc runs the main accept loop of the control service. func (s *Server) RunControlSvc(ctx context.Context, service string, tlscfg *tls.Config, - unixSocket string, unixSocketPermissions os.FileMode, tcpListen string, tcptls *tls.Config) error { + unixSocket string, unixSocketPermissions os.FileMode, tcpListen string, tcptls *tls.Config, +) error { var uli net.Listener var lock *utils.FLock var err error @@ -332,33 +357,32 @@ func (s *Server) RunControlSvc(ctx context.Context, service string, tlscfg *tls. return } if err != nil { - logger.Error("Error accepting connection: %s. Closing listener.\n", err) - _ = listener.Close() + if !strings.HasSuffix(err.Error(), "normal close") { + logger.Error("Error accepting connection: %s\n", err) + } - return + continue } go func() { + defer conn.Close() tlsConn, ok := conn.(*tls.Conn) if ok { // Explicitly run server TLS handshake so we can deal with timeout and errors here err = conn.SetDeadline(time.Now().Add(10 * time.Second)) if err != nil { logger.Error("Error setting timeout: %s. Closing socket.\n", err) - _ = conn.Close() return } err = tlsConn.Handshake() if err != nil { logger.Error("TLS handshake error: %s. Closing socket.\n", err) - _ = conn.Close() return } err = conn.SetDeadline(time.Time{}) if err != nil { logger.Error("Error clearing timeout: %s. Closing socket.\n", err) - _ = conn.Close() return } @@ -439,115 +463,3 @@ func init() { "control-service", "Run a control service", cmdlineConfigUnix{}) } } - -type Controllers struct { - UnixControl []UnixControl `mapstructure:"unix"` - TCPControl []TCPControl `mapstructure:"tcp"` -} - -func (c Controllers) Setup(ctx context.Context, cv *Server) error { - for _, c := range c.UnixControl { - if err := c.setup(ctx, cv); err != nil { - return fmt.Errorf("could not setup unix controller from controllers config: %w", err) - } - } - - for _, c := range c.TCPControl { - if err := c.setup(ctx, cv); err != nil { - return fmt.Errorf("could not setup tcp controller from controllers config: %w", err) - } - } - - return nil -} - -// UnixControl exposes a receptor control socket via unix socket. -type UnixControl struct { - // Receptor service name to listen on. - Service *string `mapstructure:"service"` - // Filename of local Unix socket to bind to the service. - File string `mapstructure:"file"` - // Socket file permissions. - Permissions *int `mapstructure:"permissions"` - // TLS config to use for the transport within receptor. - // Leave empty for no TLS. - MeshTLS *tls.ServerConf `mapstructure:"mesh-tls"` -} - -func (s *UnixControl) setup(ctx context.Context, cv *Server) error { - service := "control" - if s.Service != nil { - service = *s.Service - } - perms := 0o600 - if s.Permissions != nil { - perms = *s.Permissions - } - - var err error - var tlsReceptor *tls.Config - if s.MeshTLS != nil { - tlsReceptor, err = s.MeshTLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create receptor tls config for tcp control service %s: %w", service, err) - } - } - - return cv.RunControlSvc( - ctx, - service, - tlsReceptor, - s.File, - os.FileMode(perms), - "", - nil, - ) -} - -// TCPControl exposes a receptor control socket via TCP. -type TCPControl struct { - // Receptor service name to listen on. - Service *string `mapstructure:"service"` - // TLS config to use for the transport within receptor. - // Leave empty for no TLS. - MeshTLS *tls.ServerConf `mapstructure:"mesh-tls"` - // TLS config to use for the exposed control port.. - // Leave empty for no TLS. - TCPTLS *tls.ServerConf `mapstructure:"tcp-tls"` - // Address to listen on ("host:port" from net package). - Address string `mapstructure:"address"` -} - -func (s *TCPControl) setup(ctx context.Context, cv *Server) error { - service := "control" - if s.Service != nil { - service = *s.Service - } - - var err error - var tlsReceptor *tls.Config - var tcptls *tls.Config - if s.MeshTLS != nil { - tlsReceptor, err = s.MeshTLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create receptor tls config for tcp control service %s: %w", service, err) - } - } - - if s.TCPTLS != nil { - tcptls, err = s.TCPTLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tcp tls config for tcp control service %s: %w", service, err) - } - } - - return cv.RunControlSvc( - ctx, - service, - tlsReceptor, - "", - 0, - s.Address, - tcptls, - ) -} diff --git a/pkg/controlsvc/controlsvc_stub.go b/pkg/controlsvc/controlsvc_stub.go index 6dfd85a46..e7749762f 100644 --- a/pkg/controlsvc/controlsvc_stub.go +++ b/pkg/controlsvc/controlsvc_stub.go @@ -40,6 +40,7 @@ func (s *Server) RunControlSession(conn net.Conn) { // RunControlSvc runs the main accept loop of the control service func (s *Server) RunControlSvc(ctx context.Context, service string, tlscfg *tls.Config, - unixSocket string, unixSocketPermissions os.FileMode, tcpListen string, tcptls *tls.Config) error { + unixSocket string, unixSocketPermissions os.FileMode, tcpListen string, tcptls *tls.Config, +) error { return ErrNotImplemented } diff --git a/pkg/controlsvc/interfaces.go b/pkg/controlsvc/interfaces.go index 3ae181bd2..9975a9a72 100644 --- a/pkg/controlsvc/interfaces.go +++ b/pkg/controlsvc/interfaces.go @@ -1,6 +1,7 @@ package controlsvc import ( + "context" "io" "net" @@ -15,7 +16,7 @@ type ControlCommandType interface { // ControlCommand is an instance of a command that is being run from the control service. type ControlCommand interface { - ControlFunc(*netceptor.Netceptor, ControlFuncOperations) (map[string]interface{}, error) + ControlFunc(context.Context, *netceptor.Netceptor, ControlFuncOperations) (map[string]interface{}, error) } // ControlFuncOperations provides callbacks for control services to take actions. diff --git a/pkg/controlsvc/ping.go b/pkg/controlsvc/ping.go index 36d25f507..8e1b62d72 100644 --- a/pkg/controlsvc/ping.go +++ b/pkg/controlsvc/ping.go @@ -3,8 +3,6 @@ package controlsvc import ( "context" "fmt" - "strings" - "time" "github.com/ansible/receptor/pkg/netceptor" ) @@ -43,76 +41,8 @@ func (t *pingCommandType) InitFromJSON(config map[string]interface{}) (ControlCo return c, nil } -// ping is the internal implementation of sending a single ping packet and waiting for a reply or error. -func ping(nc *netceptor.Netceptor, target string, hopsToLive byte) (time.Duration, string, error) { - pc, err := nc.ListenPacket("") - if err != nil { - return 0, "", err - } - ctx, ctxCancel := context.WithCancel(nc.Context()) - defer func() { - ctxCancel() - _ = pc.Close() - }() - pc.SetHopsToLive(hopsToLive) - unrCh := pc.SubscribeUnreachable() - type errorResult struct { - err error - fromNode string - } - errorChan := make(chan errorResult) - go func() { - select { - case <-ctx.Done(): - return - case msg := <-unrCh: - errorChan <- errorResult{ - err: fmt.Errorf(msg.Problem), - fromNode: msg.ReceivedFromNode, - } - } - }() - startTime := time.Now() - replyChan := make(chan string) - go func() { - buf := make([]byte, 8) - _, addr, err := pc.ReadFrom(buf) - fromNode := "" - if addr != nil { - fromNode = addr.String() - fromNode = strings.TrimSuffix(fromNode, ":ping") - } - if err == nil { - select { - case replyChan <- fromNode: - case <-ctx.Done(): - } - } else { - select { - case errorChan <- errorResult{ - err: err, - fromNode: fromNode, - }: - case <-ctx.Done(): - } - } - }() - _, err = pc.WriteTo([]byte{}, nc.NewAddr(target, "ping")) - if err != nil { - return time.Since(startTime), nc.NodeID(), err - } - select { - case errRes := <-errorChan: - return time.Since(startTime), errRes.fromNode, errRes.err - case remote := <-replyChan: - return time.Since(startTime), remote, nil - case <-time.After(10 * time.Second): - return time.Since(startTime), "", fmt.Errorf("timeout") - } -} - -func (c *pingCommand) ControlFunc(nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { - pingTime, pingRemote, err := ping(nc, c.target, nc.MaxForwardingHops()) +func (c *pingCommand) ControlFunc(ctx context.Context, nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { + pingTime, pingRemote, err := nc.Ping(ctx, c.target, nc.MaxForwardingHops()) cfr := make(map[string]interface{}) if err == nil { cfr["Success"] = true diff --git a/pkg/controlsvc/reload.go b/pkg/controlsvc/reload.go index 690e0cc6a..a7b233c4e 100644 --- a/pkg/controlsvc/reload.go +++ b/pkg/controlsvc/reload.go @@ -1,6 +1,7 @@ package controlsvc import ( + "context" "fmt" "io/ioutil" "strings" @@ -157,7 +158,7 @@ func handleError(err error, errorcode int) (map[string]interface{}, error) { return cfr, nil } -func (c *reloadCommand) ControlFunc(nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { +func (c *reloadCommand) ControlFunc(ctx context.Context, nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { // Reload command stops all backends, and re-runs the ParseAndRun() on the // initial config file logger.Debug("Reloading") diff --git a/pkg/controlsvc/status.go b/pkg/controlsvc/status.go index 86741c010..271c033d3 100644 --- a/pkg/controlsvc/status.go +++ b/pkg/controlsvc/status.go @@ -1,6 +1,7 @@ package controlsvc import ( + "context" "fmt" "github.com/ansible/receptor/internal/version" @@ -46,7 +47,7 @@ func (t *statusCommandType) InitFromJSON(config map[string]interface{}) (Control return c, nil } -func (c *statusCommand) ControlFunc(nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { +func (c *statusCommand) ControlFunc(ctx context.Context, nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { status := nc.Status() statusGetters := make(map[string]func() interface{}) statusGetters["Version"] = func() interface{} { return version.Version } diff --git a/pkg/controlsvc/traceroute.go b/pkg/controlsvc/traceroute.go index 3c46300e6..24d0d01aa 100644 --- a/pkg/controlsvc/traceroute.go +++ b/pkg/controlsvc/traceroute.go @@ -1,6 +1,7 @@ package controlsvc import ( + "context" "fmt" "strconv" @@ -41,21 +42,20 @@ func (t *tracerouteCommandType) InitFromJSON(config map[string]interface{}) (Con return c, nil } -func (c *tracerouteCommand) ControlFunc(nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { +func (c *tracerouteCommand) ControlFunc(ctx context.Context, nc *netceptor.Netceptor, cfo ControlFuncOperations) (map[string]interface{}, error) { cfr := make(map[string]interface{}) - for i := 0; i <= int(nc.MaxForwardingHops()); i++ { + results := nc.Traceroute(ctx, c.target) + i := 0 + for res := range results { thisResult := make(map[string]interface{}) - pingTime, pingRemote, err := ping(nc, c.target, byte(i)) - thisResult["From"] = pingRemote - thisResult["Time"] = pingTime - thisResult["TimeStr"] = fmt.Sprint(pingTime) - if err != nil && err.Error() != netceptor.ProblemExpiredInTransit { - thisResult["Error"] = err.Error() + thisResult["From"] = res.From + thisResult["Time"] = res.Time + thisResult["TimeStr"] = fmt.Sprint(res.Time) + if res.Err != nil { + thisResult["Error"] = res.Err.Error() } cfr[strconv.Itoa(i)] = thisResult - if err == nil || err.Error() != netceptor.ProblemExpiredInTransit { - break - } + i++ } return cfr, nil diff --git a/pkg/netceptor/conn.go b/pkg/netceptor/conn.go index 59f04fef4..3ddf4dec5 100644 --- a/pkg/netceptor/conn.go +++ b/pkg/netceptor/conn.go @@ -16,10 +16,19 @@ import ( "sync" "time" - "github.com/ansible/receptor/pkg/utils" + "github.com/ansible/receptor/pkg/logger" "github.com/lucas-clemente/quic-go" ) +// MaxIdleTimeoutForQuicConnections for quic connections. The default is 30 which we have replicated here. +// This value is set on both Dial and Listen connections as the quic library would take the smallest of either connection. +var MaxIdleTimeoutForQuicConnections = 30 * time.Second + +// KeepAliveForQuicConnections is variablized to enable testing of the timeout. +// If you are doing a heartbeat your connection wont timeout without severing the connection i.e. firewall. +// Having this variablized allows the tests to set KeepAliveForQuicConnections = False so that things will properly fail. +var KeepAliveForQuicConnections = true + type acceptResult struct { conn net.Conn err error @@ -61,10 +70,11 @@ func (s *Netceptor) listen(ctx context.Context, service string, tlscfg *tls.Conf tlscfg.NextProtos = []string{"netceptor"} if tlscfg.ClientAuth == tls.RequireAndVerifyClientCert { tlscfg.GetConfigForClient = func(hi *tls.ClientHelloInfo) (*tls.Config, error) { + clientTLSCfg := tlscfg.Clone() remoteNode := strings.Split(hi.Conn.RemoteAddr().String(), ":")[0] - tlscfg.VerifyPeerCertificate = s.receptorVerifyFunc(tlscfg, remoteNode, VerifyClient) + clientTLSCfg.VerifyPeerCertificate = ReceptorVerifyFunc(tlscfg, [][]byte{}, remoteNode, ExpectedHostnameTypeReceptor, VerifyClient) - return tlscfg, nil + return clientTLSCfg, nil } } } @@ -79,7 +89,10 @@ func (s *Netceptor) listen(ctx context.Context, service string, tlscfg *tls.Conf } pc.startUnreachable() s.listenerRegistry[service] = pc - ql, err := quic.Listen(pc, tlscfg, nil) + cfg := &quic.Config{ + MaxIdleTimeout: MaxIdleTimeoutForQuicConnections, + } + ql, err := quic.Listen(pc, tlscfg, cfg) if err != nil { return nil, err } @@ -140,7 +153,6 @@ func (li *Listener) sendResult(conn net.Conn, err error) { err: err, }: case <-li.doneChan: - case <-li.doneChan: } } @@ -197,7 +209,7 @@ func (li *Listener) acceptLoop() { return } doneChan := make(chan struct{}, 1) - cctx, ccancel := utils.ContextWithCancelWithErr(li.s.context) + cctx, ccancel := context.WithCancel(li.s.context) conn := &Conn{ s: li.s, pc: li.pc, @@ -280,8 +292,9 @@ func (s *Netceptor) DialContext(ctx context.Context, node string, service string } rAddr := s.NewAddr(node, service) cfg := &quic.Config{ - HandshakeTimeout: 15 * time.Second, - KeepAlive: true, + HandshakeIdleTimeout: 15 * time.Second, + MaxIdleTimeout: MaxIdleTimeoutForQuicConnections, + KeepAlive: KeepAliveForQuicConnections, } if tlscfg == nil { tlscfg = generateClientTLSConfig() @@ -296,7 +309,7 @@ func (s *Netceptor) DialContext(ctx context.Context, node string, service string _ = pc.Close() }) } - cctx, ccancel := utils.ContextWithCancelWithErr(ctx) + cctx, ccancel := context.WithCancel(ctx) go func() { select { case <-okChan: @@ -370,18 +383,18 @@ func (s *Netceptor) DialContext(ctx context.Context, node string, service string // monitorUnreachable receives unreachable messages from the underlying PacketConn, and ends the connection // if the remote service has gone away. -func monitorUnreachable(pc *PacketConn, doneChan chan struct{}, remoteAddr Addr, cancel utils.CancelWithErrFunc) { - msgCh := pc.SubscribeUnreachable() - for { - select { - case <-pc.context.Done(): - return - case <-doneChan: - return - case msg := <-msgCh: - if msg.Problem == ProblemServiceUnknown && msg.ToNode == remoteAddr.node && msg.ToService == remoteAddr.service { - cancel(fmt.Errorf("remote service unreachable")) - } +func monitorUnreachable(pc *PacketConn, doneChan chan struct{}, remoteAddr Addr, cancel context.CancelFunc) { + msgCh := pc.SubscribeUnreachable(doneChan) + if msgCh == nil { + cancel() + + return + } + // read from channel until closed + for msg := range msgCh { + if msg.Problem == ProblemServiceUnknown && msg.ToNode == remoteAddr.node && msg.ToService == remoteAddr.service { + logger.Warning("remote service %s to node %s is unreachable", msg.ToService, msg.ToNode) + cancel() } } } @@ -410,6 +423,16 @@ func (c *Conn) Close() error { return c.qs.Close() } +func (c *Conn) CloseConnection() error { + c.pc.cancel() + c.doneOnce.Do(func() { + close(c.doneChan) + }) + logger.Debug("closing connection from service %s to %s", c.pc.localService, c.RemoteAddr().String()) + + return c.qc.CloseWithError(0, "normal close") +} + // LocalAddr returns the local address of this connection. func (c *Conn) LocalAddr() net.Addr { return c.qc.LocalAddr() diff --git a/pkg/netceptor/netceptor.go b/pkg/netceptor/netceptor.go index 61e45d1d9..9a1ef60dc 100644 --- a/pkg/netceptor/netceptor.go +++ b/pkg/netceptor/netceptor.go @@ -2,7 +2,10 @@ package netceptor import ( + "bytes" "context" + "crypto/sha256" + "crypto/sha512" "crypto/tls" "crypto/x509" "encoding/binary" @@ -98,48 +101,50 @@ const ( // Netceptor is the main object of the Receptor mesh network protocol. type Netceptor struct { - nodeID string - mtu int - routeUpdateTime time.Duration - serviceAdTime time.Duration - seenUpdateExpireTime time.Duration - maxForwardingHops byte - maxConnectionIdleTime time.Duration - workCommands []WorkCommand - epoch uint64 - sequence uint64 - connLock *sync.RWMutex - connections map[string]*connInfo - knownNodeLock *sync.RWMutex - knownNodeInfo map[string]*nodeInfo - seenUpdatesLock *sync.RWMutex - seenUpdates map[string]time.Time - knownConnectionCosts map[string]map[string]float64 - routingTableLock *sync.RWMutex - routingTable map[string]string - routingPathCosts map[string]float64 - listenerLock *sync.RWMutex - listenerRegistry map[string]*PacketConn - sendRouteFloodChan chan time.Duration - updateRoutingTableChan chan time.Duration - context context.Context - cancelFunc context.CancelFunc - hashLock *sync.RWMutex - nameHashes map[uint64]string - reservedServices map[string]func(*MessageData) error - serviceAdsLock *sync.RWMutex - serviceAdsReceived map[string]map[string]*ServiceAdvertisement - sendServiceAdsChan chan time.Duration - backendWaitGroup sync.WaitGroup - backendCount int - backendCancel []context.CancelFunc - networkName string - serverTLSConfigs map[string]*tls.Config - clientTLSConfigs map[string]*tls.Config - unreachableBroker *utils.Broker - routingUpdateBroker *utils.Broker - firewallLock *sync.RWMutex - firewallRules []FirewallRuleFunc + nodeID string + mtu int + routeUpdateTime time.Duration + serviceAdTime time.Duration + seenUpdateExpireTime time.Duration + maxForwardingHops byte + maxConnectionIdleTime time.Duration + workCommands []WorkCommand + workCommandsLock *sync.RWMutex + epoch uint64 + sequence uint64 + connLock *sync.RWMutex + connections map[string]*connInfo + knownNodeLock *sync.RWMutex + knownNodeInfo map[string]*nodeInfo + seenUpdatesLock *sync.RWMutex + seenUpdates map[string]time.Time + knownConnectionCosts map[string]map[string]float64 + routingTableLock *sync.RWMutex + routingTable map[string]string + routingPathCosts map[string]float64 + listenerLock *sync.RWMutex + listenerRegistry map[string]*PacketConn + sendRouteFloodChan chan time.Duration + updateRoutingTableChan chan time.Duration + context context.Context + cancelFunc context.CancelFunc + hashLock *sync.RWMutex + nameHashes map[uint64]string + reservedServices map[string]func(*MessageData) error + serviceAdsLock *sync.RWMutex + serviceAdsReceived map[string]map[string]*ServiceAdvertisement + sendServiceAdsChan chan time.Duration + backendWaitGroup sync.WaitGroup + backendCount int + backendCancel []context.CancelFunc + networkName string + serverTLSConfigs map[string]*tls.Config + clientTLSConfigs map[string]*tls.Config + clientPinnedFingerprints map[string][][]byte + unreachableBroker *utils.Broker + routingUpdateBroker *utils.Broker + firewallLock *sync.RWMutex + firewallRules []FirewallRuleFunc } // ConnStatus holds information about a single connection in the Status struct. @@ -195,6 +200,7 @@ type connInfo struct { CancelFunc context.CancelFunc Cost float64 lastReceivedData time.Time + lastReceivedLock *sync.RWMutex } type nodeInfo struct { @@ -292,43 +298,46 @@ func makeNetworkName(nodeID string) string { // NewWithConsts constructs a new Receptor network protocol instance, specifying operational constants. func NewWithConsts(ctx context.Context, nodeID string, mtu int, routeUpdateTime time.Duration, serviceAdTime time.Duration, seenUpdateExpireTime time.Duration, - maxForwardingHops byte, maxConnectionIdleTime time.Duration) *Netceptor { + maxForwardingHops byte, maxConnectionIdleTime time.Duration, +) *Netceptor { s := Netceptor{ - nodeID: nodeID, - mtu: mtu, - routeUpdateTime: routeUpdateTime, - serviceAdTime: serviceAdTime, - seenUpdateExpireTime: seenUpdateExpireTime, - maxForwardingHops: maxForwardingHops, - maxConnectionIdleTime: maxConnectionIdleTime, - epoch: uint64(time.Now().Unix()*(1<<24)) + uint64(rand.Intn(1<<24)), - sequence: 0, - connLock: &sync.RWMutex{}, - connections: make(map[string]*connInfo), - knownNodeLock: &sync.RWMutex{}, - knownNodeInfo: make(map[string]*nodeInfo), - seenUpdatesLock: &sync.RWMutex{}, - seenUpdates: make(map[string]time.Time), - knownConnectionCosts: make(map[string]map[string]float64), - routingTableLock: &sync.RWMutex{}, - routingTable: make(map[string]string), - routingPathCosts: make(map[string]float64), - listenerLock: &sync.RWMutex{}, - listenerRegistry: make(map[string]*PacketConn), - sendRouteFloodChan: nil, - updateRoutingTableChan: nil, - hashLock: &sync.RWMutex{}, - nameHashes: make(map[uint64]string), - serviceAdsLock: &sync.RWMutex{}, - serviceAdsReceived: make(map[string]map[string]*ServiceAdvertisement), - sendServiceAdsChan: nil, - backendWaitGroup: sync.WaitGroup{}, - backendCount: 0, - backendCancel: nil, - networkName: makeNetworkName(nodeID), - clientTLSConfigs: make(map[string]*tls.Config), - serverTLSConfigs: make(map[string]*tls.Config), - firewallLock: &sync.RWMutex{}, + nodeID: nodeID, + mtu: mtu, + routeUpdateTime: routeUpdateTime, + serviceAdTime: serviceAdTime, + seenUpdateExpireTime: seenUpdateExpireTime, + maxForwardingHops: maxForwardingHops, + maxConnectionIdleTime: maxConnectionIdleTime, + epoch: uint64(time.Now().Unix()*(1<<24)) + uint64(rand.Intn(1<<24)), + sequence: 0, + connLock: &sync.RWMutex{}, + connections: make(map[string]*connInfo), + knownNodeLock: &sync.RWMutex{}, + knownNodeInfo: make(map[string]*nodeInfo), + seenUpdatesLock: &sync.RWMutex{}, + seenUpdates: make(map[string]time.Time), + knownConnectionCosts: make(map[string]map[string]float64), + routingTableLock: &sync.RWMutex{}, + routingTable: make(map[string]string), + routingPathCosts: make(map[string]float64), + listenerLock: &sync.RWMutex{}, + listenerRegistry: make(map[string]*PacketConn), + sendRouteFloodChan: nil, + updateRoutingTableChan: nil, + hashLock: &sync.RWMutex{}, + nameHashes: make(map[uint64]string), + serviceAdsLock: &sync.RWMutex{}, + serviceAdsReceived: make(map[string]map[string]*ServiceAdvertisement), + sendServiceAdsChan: nil, + backendWaitGroup: sync.WaitGroup{}, + backendCount: 0, + backendCancel: nil, + networkName: makeNetworkName(nodeID), + clientTLSConfigs: make(map[string]*tls.Config), + clientPinnedFingerprints: make(map[string][][]byte), + serverTLSConfigs: make(map[string]*tls.Config), + firewallLock: &sync.RWMutex{}, + workCommandsLock: &sync.RWMutex{}, } s.reservedServices = map[string]func(*MessageData) error{ "ping": s.handlePing, @@ -570,9 +579,11 @@ func (s *Netceptor) Status() Status { adCopy := *ad if adCopy.NodeID == s.nodeID { adCopy.Time = time.Now() + s.workCommandsLock.RLock() if len(s.workCommands) > 0 { adCopy.WorkCommands = s.workCommands } + s.workCommandsLock.RUnlock() } serviceAds = append(serviceAds, &adCopy) } @@ -623,7 +634,6 @@ func (s *Netceptor) AddFirewallRules(rules []FirewallRuleFunc, clearExisting boo func (s *Netceptor) addLocalServiceAdvertisement(service string, connType byte, tags map[string]string) { s.serviceAdsLock.Lock() - defer s.serviceAdsLock.Unlock() n, ok := s.serviceAdsReceived[s.nodeID] if !ok { n = make(map[string]*ServiceAdvertisement) @@ -636,7 +646,13 @@ func (s *Netceptor) addLocalServiceAdvertisement(service string, connType byte, ConnType: connType, Tags: tags, } - s.sendServiceAdsChan <- 0 + s.serviceAdsLock.Unlock() + select { + case <-s.context.Done(): + return + case s.sendServiceAdsChan <- 0: + default: + } } func (s *Netceptor) removeLocalServiceAdvertisement(service string) error { @@ -697,9 +713,11 @@ func (s *Netceptor) sendServiceAds() { } if svcType, ok := sa.Tags["type"]; ok { if svcType == "Control Service" { + s.workCommandsLock.RLock() if len(s.workCommands) > 0 { sa.WorkCommands = s.workCommands } + s.workCommandsLock.RUnlock() } } ads = append(ads, sa) @@ -722,9 +740,12 @@ func (s *Netceptor) monitorConnectionAging() { timedOut := make([]context.CancelFunc, 0) s.connLock.RLock() for i := range s.connections { - if time.Since(s.connections[i].lastReceivedData) > s.maxConnectionIdleTime { + conn := s.connections[i] + conn.lastReceivedLock.RLock() + if time.Since(conn.lastReceivedData) > s.maxConnectionIdleTime { timedOut = append(timedOut, s.connections[i].CancelFunc) } + conn.lastReceivedLock.RUnlock() } s.connLock.RUnlock() for i := range timedOut { @@ -851,17 +872,18 @@ func (s *Netceptor) SubscribeRoutingUpdates() chan map[string]string { // Forwards a message to all neighbors, possibly excluding one. func (s *Netceptor) flood(message []byte, excludeConn string) { s.connLock.RLock() - writeChans := make([]chan []byte, 0) - for conn, connInfo := range s.connections { + for conn, ci := range s.connections { if conn != excludeConn { - writeChans = append(writeChans, connInfo.WriteChan) + go func(ci *connInfo) { + select { + case ci.WriteChan <- message: + case <-ci.Context.Done(): + logger.Debug("connInfo cancelled during flood write") + } + }(ci) } } s.connLock.RUnlock() - for i := range writeChans { - i := i - go func() { writeChans[i] <- message }() - } } // GetServerTLSConfig retrieves a server TLS config by name. @@ -883,6 +905,8 @@ func (s *Netceptor) AddWorkCommand(command string, secure bool) error { return fmt.Errorf("must provide a name") } wC := WorkCommand{WorkType: command, Secure: secure} + s.workCommandsLock.Lock() + defer s.workCommandsLock.Unlock() s.workCommands = append(s.workCommands, wC) return nil @@ -918,17 +942,36 @@ func (rce ReceptorCertNameError) Error() string { plural, strings.Join(rce.ValidNodes, ", "), rce.ExpectedNode) } +// VerifyType indicates whether we are verifying a server or client. +type VerifyType int + const ( // VerifyServer indicates we are the client, verifying a server. - VerifyServer = 1 + VerifyServer VerifyType = 1 // VerifyClient indicates we are the server, verifying a client. VerifyClient = 2 ) -// receptorVerifyFunc generates a function that verifies a Receptor node ID. -func (s *Netceptor) receptorVerifyFunc(tlscfg *tls.Config, expectedNodeID string, - verifyType int) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { +// ExpectedHostnameType indicates whether we are connecting to a DNS hostname or a Receptor Node ID. +type ExpectedHostnameType int + +const ( + // ExpectedHostnameTypeDNS indicates we are expecting a DNS style hostname. + ExpectedHostnameTypeDNS ExpectedHostnameType = 1 + // ExpectedHostnameTypeReceptor indicates we are expecting a Receptor node ID. + ExpectedHostnameTypeReceptor = 2 +) + +// ReceptorVerifyFunc generates a function that verifies a Receptor node ID. +func ReceptorVerifyFunc(tlscfg *tls.Config, pinnedFingerprints [][]byte, expectedHostname string, + expectedHostnameType ExpectedHostnameType, verifyType VerifyType, +) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if len(rawCerts) == 0 { + logger.Error("RVF failed: peer certificate missing") + + return fmt.Errorf("RVF failed: peer certificate missing") + } certs := make([]*x509.Certificate, len(rawCerts)) for i, asn1Data := range rawCerts { cert, err := x509.ParseCertificate(asn1Data) @@ -948,6 +991,9 @@ func (s *Netceptor) receptorVerifyFunc(tlscfg *tls.Config, expectedNodeID string CurrentTime: time.Now(), KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } + if expectedHostnameType == ExpectedHostnameTypeDNS && expectedHostname != "" { + opts.DNSName = expectedHostname + } case VerifyClient: opts = x509.VerifyOptions{ Intermediates: x509.NewCertPool(), @@ -955,8 +1001,72 @@ func (s *Netceptor) receptorVerifyFunc(tlscfg *tls.Config, expectedNodeID string CurrentTime: time.Now(), KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } + if expectedHostnameType == ExpectedHostnameTypeDNS && expectedHostname != "" { + opts.DNSName = expectedHostname + } default: - return fmt.Errorf("invalid verification type: must be client or server") + logger.Error("RVF failed: invalid verification type: must be client or server") + + return fmt.Errorf("RVF failed: invalid verification type: must be client or server") + } + + if len(pinnedFingerprints) > 0 { + var sha224sum []byte + var sha256sum []byte + var sha384sum []byte + var sha512sum []byte + fingerprintOK := false + for _, fing := range pinnedFingerprints { + fingLenFound := false + for _, s := range []struct { + len int + sum *[]byte + sumFunc func(data []byte) []byte + }{ + {28, &sha224sum, func(data []byte) []byte { + sum := sha256.Sum224(data) + + return sum[:] + }}, + {32, &sha256sum, func(data []byte) []byte { + sum := sha256.Sum256(data) + + return sum[:] + }}, + {48, &sha384sum, func(data []byte) []byte { + sum := sha512.Sum384(data) + + return sum[:] + }}, + {64, &sha512sum, func(data []byte) []byte { + sum := sha512.Sum512(data) + + return sum[:] + }}, + } { + if len(fing) == s.len { + fingLenFound = true + if *s.sum == nil { + *s.sum = s.sumFunc(certs[0].Raw) + } + if bytes.Equal(fing, *s.sum) { + fingerprintOK = true + + break + } + } + } + if !fingLenFound { + logger.Error("RVF failed: pinned certificate must be sha224, sha256, sha384 or sha512") + + return fmt.Errorf("RVF failed: pinned certificate must be sha224, sha256, sha384 or sha512") + } + } + if !fingerprintOK { + logger.Error("RVF failed: presented certificate does not match any pinned fingerprint") + + return fmt.Errorf("RVF failed: presented certificate does not match any pinned fingerprint") + } } for _, cert := range certs[1:] { @@ -969,25 +1079,28 @@ func (s *Netceptor) receptorVerifyFunc(tlscfg *tls.Config, expectedNodeID string return err } - var receptorNames []string - receptorNames, err = utils.ReceptorNames(certs[0].Extensions) - if err != nil { - logger.Error("RVF failed to get ReceptorNames: %s", err) - return err - } - found := false - for _, receptorName := range receptorNames { - if receptorName == expectedNodeID { - found = true + if expectedHostnameType == ExpectedHostnameTypeReceptor { + var receptorNames []string + receptorNames, err = utils.ReceptorNames(certs[0].Extensions) + if err != nil { + logger.Error("RVF failed to get ReceptorNames: %s", err) - break + return err } - } - if !found { - logger.Error("RVF ReceptorNameError: %s", err) + found := false + for _, receptorName := range receptorNames { + if receptorName == expectedHostname { + found = true + + break + } + } + if !found { + logger.Error("RVF ReceptorNameError: expected %s but found %s", expectedHostname, strings.Join(receptorNames, ", ")) - return ReceptorCertNameError{ValidNodes: receptorNames, ExpectedNode: expectedNodeID} + return ReceptorCertNameError{ValidNodes: receptorNames, ExpectedNode: expectedHostname} + } } return nil @@ -996,7 +1109,7 @@ func (s *Netceptor) receptorVerifyFunc(tlscfg *tls.Config, expectedNodeID string // GetClientTLSConfig retrieves a client TLS config by name. Supported host name types // are dns and receptor. -func (s *Netceptor) GetClientTLSConfig(name string, expectedHostName string, expectedHostNameType string) (*tls.Config, error) { +func (s *Netceptor) GetClientTLSConfig(name string, expectedHostName string, expectedHostNameType ExpectedHostnameType) (*tls.Config, error) { if name == "" { return nil, nil } @@ -1004,26 +1117,32 @@ func (s *Netceptor) GetClientTLSConfig(name string, expectedHostName string, exp if !ok { return nil, fmt.Errorf("unknown TLS config %s", name) } + var pinnedFingerprints [][]byte + pinnedFingerprints, ok = s.clientPinnedFingerprints[name] + if !ok { + return nil, fmt.Errorf("pinned fingerprints missing for %s", name) + } tlscfg = tlscfg.Clone() - switch { - case tlscfg.InsecureSkipVerify: - // noop - case expectedHostNameType == "receptor": - tlscfg.InsecureSkipVerify = true - tlscfg.VerifyPeerCertificate = s.receptorVerifyFunc(tlscfg, expectedHostName, VerifyServer) - default: - tlscfg.ServerName = expectedHostName + if !tlscfg.InsecureSkipVerify { + tlscfg.VerifyPeerCertificate = ReceptorVerifyFunc(tlscfg, pinnedFingerprints, expectedHostName, expectedHostNameType, VerifyServer) + switch expectedHostNameType { + case ExpectedHostnameTypeDNS: + tlscfg.ServerName = expectedHostName + case ExpectedHostnameTypeReceptor: + tlscfg.InsecureSkipVerify = true + } } return tlscfg, nil } // SetClientTLSConfig stores a client TLS config by name. -func (s *Netceptor) SetClientTLSConfig(name string, config *tls.Config) error { +func (s *Netceptor) SetClientTLSConfig(name string, config *tls.Config, pinnedFingerprints [][]byte) error { if name == "" { return fmt.Errorf("must provide a name") } s.clientTLSConfigs[name] = config + s.clientPinnedFingerprints[name] = pinnedFingerprints return nil } @@ -1041,8 +1160,8 @@ func (s *Netceptor) addNameHash(name string) uint64 { hv := h.Sum64() s.hashLock.Lock() defer s.hashLock.Unlock() - _, ok := s.nameHashes[hv] - if !ok { + + if _, ok := s.nameHashes[hv]; !ok { s.nameHashes[hv] = name } @@ -1157,7 +1276,11 @@ func (s *Netceptor) forwardMessage(md *MessageData) error { // decrement HopsToLive message[1]-- logger.Trace(" Forwarding data length %d via %s\n", len(md.Data), nextHop) - c.WriteChan <- message + select { + case <-c.Context.Done(): + return fmt.Errorf("connInfo cancelled while forwarding message") + case c.WriteChan <- message: + } return nil } @@ -1233,13 +1356,13 @@ func (s *Netceptor) printRoutingTable() { // Constructs a routing update message. func (s *Netceptor) makeRoutingUpdate(suspectedDuplicate uint64) *routingUpdate { + s.connLock.Lock() + defer s.connLock.Unlock() s.sequence++ - s.connLock.RLock() conns := make(map[string]float64) for conn := range s.connections { conns[conn] = s.connections[conn].Cost } - s.connLock.RUnlock() update := &routingUpdate{ NodeID: s.nodeID, UpdateID: randstr.RandomString(8), @@ -1268,7 +1391,10 @@ func (s *Netceptor) translateStructToNetwork(messageType byte, content interface // Sends a routing update to all neighbors. func (s *Netceptor) sendRoutingUpdate(suspectedDuplicate uint64) { - if len(s.connections) == 0 { + s.connLock.RLock() + connCount := len(s.connections) + s.connLock.RUnlock() + if connCount == 0 { return } ru := s.makeRoutingUpdate(suspectedDuplicate) @@ -1352,7 +1478,11 @@ func (s *Netceptor) handleRoutingUpdate(ri *routingUpdate, recvConn string) { return } } else { - s.sendRouteFloodChan <- 0 + select { + case <-s.context.Done(): + return + case s.sendRouteFloodChan <- 0: + } ni = &nodeInfo{} } ni.Epoch = ri.UpdateEpoch @@ -1383,7 +1513,11 @@ func (s *Netceptor) handleRoutingUpdate(ri *routingUpdate, recvConn string) { } s.knownNodeLock.Unlock() if changed { - s.updateRoutingTableChan <- 100 * time.Millisecond + select { + case <-s.context.Done(): + return + case s.updateRoutingTableChan <- 100 * time.Millisecond: + } } } ri.ForwardingNode = s.nodeID @@ -1496,8 +1630,12 @@ func (s *Netceptor) handleMessageData(md *MessageData) error { return nil } - pc.recvChan <- md s.listenerLock.RUnlock() + select { + case <-pc.context.Done(): + return nil + case pc.recvChan <- md: + } return nil } @@ -1567,11 +1705,6 @@ func (s *Netceptor) handleServiceAdvertisement(data []byte, receivedFrom string) func (ci *connInfo) protoReader(sess BackendSession) { for { buf, err := sess.Recv(1 * time.Second) - select { - case <-ci.Context.Done(): - return - default: - } if err == ErrTimeout { continue } @@ -1583,8 +1716,14 @@ func (ci *connInfo) protoReader(sess BackendSession) { return } + ci.lastReceivedLock.Lock() ci.lastReceivedData = time.Now() - ci.ReadChan <- buf + ci.lastReceivedLock.Unlock() + select { + case <-ci.Context.Done(): + return + case ci.ReadChan <- buf: + } } } @@ -1622,7 +1761,13 @@ func (s *Netceptor) sendInitialConnectMessage(ci *connInfo, initDoneChan chan bo return } logger.Debug("Sending initial connection message\n") - ci.WriteChan <- ri + select { + case ci.WriteChan <- ri: + case <-ci.Context.Done(): + return + case <-initDoneChan: + return + } count++ if count > 10 { logger.Warning("Giving up on connection initialization\n") @@ -1643,15 +1788,18 @@ func (s *Netceptor) sendInitialConnectMessage(ci *connInfo, initDoneChan chan bo } } -func (s *Netceptor) sendRejectMessage(writeChan chan []byte) { +func (s *Netceptor) sendRejectMessage(ci *connInfo) { rejMsg, err := s.translateStructToNetwork(MsgTypeReject, make([]string, 0)) if err != nil { - writeChan <- rejMsg + select { + case <-ci.Context.Done(): + case ci.WriteChan <- rejMsg: + } } } func (s *Netceptor) sendAndLogConnectionRejection(remoteNodeID string, ci *connInfo, reason string) error { - s.sendRejectMessage(ci.WriteChan) + s.sendRejectMessage(ci) return fmt.Errorf("rejected connection with node %s because %s", remoteNodeID, reason) } @@ -1675,22 +1823,24 @@ func (s *Netceptor) runProtocol(ctx context.Context, sess BackendSession, bi *ba delete(s.knownConnectionCosts[remoteNodeID], s.nodeID) delete(s.knownConnectionCosts[s.nodeID], remoteNodeID) s.knownNodeLock.Unlock() - done := false + select { - case <-ctx.Done(): - done = true - default: + case s.sendRouteFloodChan <- 0: + case <-ctx.Done(): // ctx is a child of s.context + return } - if !done { - s.updateRoutingTableChan <- 0 - s.sendRouteFloodChan <- 0 + select { + case s.updateRoutingTableChan <- 0: + case <-ctx.Done(): + return } } }() ci := &connInfo{ - ReadChan: make(chan []byte), - WriteChan: make(chan []byte), - Cost: connectionCost, + ReadChan: make(chan []byte), + WriteChan: make(chan []byte), + Cost: connectionCost, + lastReceivedLock: &sync.RWMutex{}, } ci.Context, ci.CancelFunc = context.WithCancel(ctx) go ci.protoReader(sess) diff --git a/pkg/netceptor/netceptor_test.go b/pkg/netceptor/netceptor_test.go index 583bd334b..b625ffc68 100644 --- a/pkg/netceptor/netceptor_test.go +++ b/pkg/netceptor/netceptor_test.go @@ -17,7 +17,9 @@ import ( type logWriter struct { t *testing.T node1count int + node1Lock sync.RWMutex node2count int + node2Lock sync.RWMutex } func (lw *logWriter) Write(p []byte) (n int, err error) { @@ -31,9 +33,13 @@ func (lw *logWriter) Write(p []byte) (n int, err error) { } } else if strings.HasPrefix(s, "TRACE") { if strings.Contains(s, "via node1") { + lw.node1Lock.Lock() lw.node1count++ + lw.node1Lock.Unlock() } else if strings.Contains(s, "via node2") { + lw.node2Lock.Lock() lw.node2count++ + lw.node2Lock.Unlock() } } lw.t.Log(s) @@ -137,7 +143,10 @@ func TestHopCountLimit(t *testing.T) { if !ok { t.Fatal("node2 disappeared from node1's connections") } - if time.Since(c.lastReceivedData) > 250*time.Millisecond { + c.lastReceivedLock.RLock() + lastReceivedData := c.lastReceivedData + c.lastReceivedLock.RUnlock() + if time.Since(lastReceivedData) > 250*time.Millisecond { break } select { @@ -148,7 +157,13 @@ func TestHopCountLimit(t *testing.T) { } // Make sure we actually succeeded in creating a routing loop - if lw.node1count < 10 || lw.node2count < 10 { + lw.node1Lock.RLock() + node1Count := lw.node1count + lw.node1Lock.RUnlock() + lw.node2Lock.RLock() + node2Count := lw.node2count + lw.node2Lock.RUnlock() + if node1Count < 10 || node2Count < 10 { t.Fatal("test did not create a routing loop") } @@ -219,8 +234,10 @@ func TestLotsOfPings(t *testing.T) { } responses := make([][]bool, len(nodes)) + responseLocks := make([][]sync.RWMutex, len(nodes)) for i := range nodes { responses[i] = make([]bool, len(nodes)) + responseLocks[i] = make([]sync.RWMutex, len(nodes)) } errorChan := make(chan error) @@ -228,6 +245,9 @@ func TestLotsOfPings(t *testing.T) { wg := sync.WaitGroup{} for i := range nodes { for j := range nodes { + // Need to make copies of these variables to avoid a data race + i2 := i + j2 := j wg.Add(2) go func(sender *Netceptor, recipient *Netceptor, response *bool) { pc, err := sender.ListenPacket("") @@ -265,7 +285,9 @@ func TestLotsOfPings(t *testing.T) { return } t.Logf("%s received response from %s", sender.nodeID, recipient.nodeID) + responseLocks[i2][j2].Lock() *response = true + responseLocks[i2][j2].Unlock() } }() go func() { @@ -279,7 +301,10 @@ func TestLotsOfPings(t *testing.T) { return case <-time.After(100 * time.Millisecond): } - if *response { + responseLocks[i2][j2].RLock() + r := *response + responseLocks[i2][j2].RUnlock() + if r { return } } @@ -295,7 +320,10 @@ func TestLotsOfPings(t *testing.T) { good := true for i := range nodes { for j := range nodes { - if !responses[i][j] { + responseLocks[i][j].RLock() + r := responses[i][j] + responseLocks[i][j].RUnlock() + if !r { good = false break @@ -446,7 +474,7 @@ func TestDuplicateNodeDetection(t *testing.T) { }() select { case <-backendCloseChan: - case <-time.After(60 * time.Second): + case <-time.After(120 * time.Second): t.Fatal("timed out waiting for duplicate node to terminate") } @@ -556,18 +584,22 @@ func TestFirewalling(t *testing.T) { } // Subscribe for unreachable messages - unreach2chan := pc2.SubscribeUnreachable() + doneChan := make(chan struct{}) + unreach2chan := pc2.SubscribeUnreachable(doneChan) // Save received unreachable messages to a variable var lastUnreachMsg *UnreachableNotification + lastUnreachLock := sync.RWMutex{} go func() { - for { - select { - case <-timeout.Done(): - return - case unreach := <-unreach2chan: - lastUnreachMsg = &unreach - } + <-timeout.Done() + close(doneChan) + }() + go func() { + for unreach := range unreach2chan { + unreach := unreach + lastUnreachLock.Lock() + lastUnreachMsg = &unreach + lastUnreachLock.Unlock() } }() @@ -611,7 +643,10 @@ func TestFirewalling(t *testing.T) { t.Fatal(err) } time.Sleep(100 * time.Millisecond) - if lastUnreachMsg == nil { + lastUnreachLock.RLock() + lum := lastUnreachMsg //nolint:ifshort + lastUnreachLock.RUnlock() + if lum == nil { t.Fatal("did not receive expected unreachable message") } @@ -715,18 +750,22 @@ func TestAllowedPeers(t *testing.T) { } // Subscribe for unreachable messages - unreach2chan := pc2.SubscribeUnreachable() + doneChan := make(chan struct{}) + unreach2chan := pc2.SubscribeUnreachable(doneChan) // Save received unreachable messages to a variable var lastUnreachMsg *UnreachableNotification + lastUnreachLock := sync.RWMutex{} go func() { - for { - select { - case <-timeout.Done(): - return - case unreach := <-unreach2chan: - lastUnreachMsg = &unreach - } + <-timeout.Done() + close(doneChan) + }() + go func() { + for unreach := range unreach2chan { + unreach := unreach + lastUnreachLock.Lock() + lastUnreachMsg = &unreach + lastUnreachLock.Unlock() } }() @@ -770,7 +809,10 @@ func TestAllowedPeers(t *testing.T) { t.Fatal(err) } time.Sleep(100 * time.Millisecond) - if lastUnreachMsg == nil { + lastUnreachLock.RLock() + lum := lastUnreachMsg //nolint:ifshort + lastUnreachLock.RUnlock() + if lum == nil { t.Fatal("did not receive expected unreachable message") } diff --git a/pkg/netceptor/packetconn.go b/pkg/netceptor/packetconn.go index eb371a148..08722d9de 100644 --- a/pkg/netceptor/packetconn.go +++ b/pkg/netceptor/packetconn.go @@ -12,18 +12,17 @@ import ( // PacketConn implements the net.PacketConn interface via the Receptor network. type PacketConn struct { - s *Netceptor - localService string - recvChan chan *MessageData - readDeadline time.Time - advertise bool - adTags map[string]string - connType byte - hopsToLive byte - unreachableMsgChan chan interface{} - unreachableSubs *utils.Broker - context context.Context - cancel context.CancelFunc + s *Netceptor + localService string + recvChan chan *MessageData + readDeadline time.Time + advertise bool + adTags map[string]string + connType byte + hopsToLive byte + unreachableSubs *utils.Broker + context context.Context + cancel context.CancelFunc } // ListenPacket returns a datagram connection compatible with Go's net.PacketConn. @@ -76,50 +75,59 @@ func (s *Netceptor) ListenPacketAndAdvertise(service string, tags map[string]str func (pc *PacketConn) startUnreachable() { pc.context, pc.cancel = context.WithCancel(pc.s.context) pc.unreachableSubs = utils.NewBroker(pc.context, reflect.TypeOf(UnreachableNotification{})) - pc.unreachableMsgChan = pc.s.unreachableBroker.Subscribe() + iChan := pc.s.unreachableBroker.Subscribe() go func() { - for { - select { - case <-pc.context.Done(): - return - case msgIf := <-pc.unreachableMsgChan: - msg, ok := msgIf.(UnreachableNotification) - if !ok { - continue - } - FromNode := msg.FromNode - FromService := msg.FromService - if FromNode == pc.s.nodeID && FromService == pc.localService { - _ = pc.unreachableSubs.Publish(msg) - } + <-pc.context.Done() + pc.s.unreachableBroker.Unsubscribe(iChan) + }() + go func() { + for msgIf := range iChan { + msg, ok := msgIf.(UnreachableNotification) + if !ok { + continue + } + FromNode := msg.FromNode + FromService := msg.FromService + if FromNode == pc.s.nodeID && FromService == pc.localService { + _ = pc.unreachableSubs.Publish(msg) } } }() } // SubscribeUnreachable subscribes for unreachable messages relevant to this PacketConn. -func (pc *PacketConn) SubscribeUnreachable() chan UnreachableNotification { +func (pc *PacketConn) SubscribeUnreachable(doneChan chan struct{}) chan UnreachableNotification { iChan := pc.unreachableSubs.Subscribe() + if iChan == nil { + return nil + } uChan := make(chan UnreachableNotification) + // goroutine 1 + // if doneChan is selected, this will unsubscribe the channel, which should + // eventually close out the go routine 2 + go func() { + select { + case <-doneChan: + pc.unreachableSubs.Unsubscribe(iChan) + case <-pc.context.Done(): + } + }() + // goroutine 2 + // this will exit when either the broker closes iChan, or the broker + // returns via pc.context.Done() go func() { for { - select { - case msgIf, ok := <-iChan: - if !ok { - close(uChan) - - return - } - msg, ok := msgIf.(UnreachableNotification) - if !ok { - continue - } - uChan <- msg - case <-pc.context.Done(): + msgIf, ok := <-iChan + if !ok { close(uChan) return } + msg, ok := msgIf.(UnreachableNotification) + if !ok { + continue + } + uChan <- msg } }() @@ -130,7 +138,11 @@ func (pc *PacketConn) SubscribeUnreachable() chan UnreachableNotification { func (pc *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { var m *MessageData if pc.readDeadline.IsZero() { - m = <-pc.recvChan + select { + case m = <-pc.recvChan: + case <-pc.context.Done(): + return 0, nil, fmt.Errorf("connection context closed") + } } else { select { case m = <-pc.recvChan: diff --git a/pkg/netceptor/ping.go b/pkg/netceptor/ping.go new file mode 100644 index 000000000..d1836c2db --- /dev/null +++ b/pkg/netceptor/ping.go @@ -0,0 +1,118 @@ +package netceptor + +import ( + "context" + "fmt" + "strings" + "time" +) + +// Ping sends a single test packet and waits for a reply or error. +func (s *Netceptor) Ping(ctx context.Context, target string, hopsToLive byte) (time.Duration, string, error) { + pc, err := s.ListenPacket("") + if err != nil { + return 0, "", err + } + ctxPing, ctxCancel := context.WithCancel(ctx) + defer func() { + ctxCancel() + _ = pc.Close() + }() + pc.SetHopsToLive(hopsToLive) + doneChan := make(chan struct{}) + unrCh := pc.SubscribeUnreachable(doneChan) + defer close(doneChan) + type errorResult struct { + err error + fromNode string + } + errorChan := make(chan errorResult) + go func() { + for msg := range unrCh { + errorChan <- errorResult{ + err: fmt.Errorf(msg.Problem), + fromNode: msg.ReceivedFromNode, + } + } + }() + startTime := time.Now() + replyChan := make(chan string) + go func() { + buf := make([]byte, 8) + _, addr, err := pc.ReadFrom(buf) + fromNode := "" + if addr != nil { + fromNode = addr.String() + fromNode = strings.TrimSuffix(fromNode, ":ping") + } + if err == nil { + select { + case replyChan <- fromNode: + case <-ctxPing.Done(): + case <-s.context.Done(): + } + } else { + select { + case errorChan <- errorResult{ + err: err, + fromNode: fromNode, + }: + case <-ctx.Done(): + case <-s.context.Done(): + } + } + }() + _, err = pc.WriteTo([]byte{}, s.NewAddr(target, "ping")) + if err != nil { + return time.Since(startTime), s.NodeID(), err + } + select { + case errRes := <-errorChan: + return time.Since(startTime), errRes.fromNode, errRes.err + case remote := <-replyChan: + return time.Since(startTime), remote, nil + case <-time.After(10 * time.Second): + return time.Since(startTime), "", fmt.Errorf("timeout") + case <-ctxPing.Done(): + return time.Since(startTime), "", fmt.Errorf("user cancelled") + case <-s.context.Done(): + return time.Since(startTime), "", fmt.Errorf("netceptor shutdown") + } +} + +// TracerouteResult is the result of one hop of a traceroute. +type TracerouteResult struct { + From string + Time time.Duration + Err error +} + +// Traceroute returns a channel which will receive a series of hops between this node and the target. +func (s *Netceptor) Traceroute(ctx context.Context, target string) <-chan *TracerouteResult { + results := make(chan *TracerouteResult) + go func() { + defer close(results) + for i := 0; i <= int(s.MaxForwardingHops()); i++ { + pingTime, pingRemote, err := s.Ping(ctx, target, byte(i)) + res := &TracerouteResult{ + From: pingRemote, + Time: pingTime, + } + if err != nil && err.Error() != ProblemExpiredInTransit { + res.Err = err + } + select { + case results <- res: + case <-ctx.Done(): + return + case <-s.context.Done(): + return + } + if res.Err != nil || err == nil { + return + } + } + }() + + return results +} diff --git a/pkg/netceptor/tlsconfig.go b/pkg/netceptor/tlsconfig.go index a08e4a68a..22edefeec 100644 --- a/pkg/netceptor/tlsconfig.go +++ b/pkg/netceptor/tlsconfig.go @@ -6,8 +6,10 @@ package netceptor import ( "crypto/tls" "crypto/x509" + "encoding/hex" "fmt" "io/ioutil" + "strings" "github.com/ghjm/cmdline" ) @@ -21,20 +23,37 @@ var configSection = &cmdline.ConfigSection{ Order: 5, } +func decodeFingerprints(fingerprints []string) ([][]byte, error) { + fingerprintBytes := make([][]byte, 0, len(fingerprints)) + for _, fingStr := range fingerprints { + fingBytes, err := hex.DecodeString(strings.ReplaceAll(fingStr, ":", "")) + if err != nil { + return nil, fmt.Errorf("error decoding fingerprint") + } + if len(fingBytes) != 32 && len(fingBytes) != 64 { + return nil, fmt.Errorf("fingerprints must be 32 or 64 bytes for sha256 or sha512") + } + fingerprintBytes = append(fingerprintBytes, fingBytes) + } + + return fingerprintBytes, nil +} + // tlsServerCfg stores the configuration options for a TLS server. type tlsServerCfg struct { - Name string `required:"true" description:"Name of this TLS server configuration"` - Cert string `required:"true" description:"Server certificate filename"` - Key string `required:"true" description:"Server private key filename"` - RequireClientCert bool `description:"Require client certificates" default:"false"` - ClientCAs string `description:"Filename of CA bundle to verify client certs with"` + Name string `required:"true" description:"Name of this TLS server configuration"` + Cert string `required:"true" description:"Server certificate filename"` + Key string `required:"true" description:"Server private key filename"` + RequireClientCert bool `description:"Require client certificates" default:"false"` + ClientCAs string `description:"Filename of CA bundle to verify client certs with"` + PinnedClientCert []string `description:"Pinned fingerprint of required client certificate"` } // Prepare creates the tls.config and stores it in the global map. func (cfg tlsServerCfg) Prepare() error { tlscfg := &tls.Config{ - MinVersion: tls.VersionTLS12, PreferServerCipherSuites: true, + MinVersion: tls.VersionTLS12, } certbytes, err := ioutil.ReadFile(cfg.Cert) @@ -53,12 +72,12 @@ func (cfg tlsServerCfg) Prepare() error { tlscfg.Certificates = []tls.Certificate{cert} if cfg.ClientCAs != "" { - bytes, err := ioutil.ReadFile(cfg.ClientCAs) + caBytes, err := ioutil.ReadFile(cfg.ClientCAs) if err != nil { return fmt.Errorf("error reading client CAs file: %s", err) } clientCAs := x509.NewCertPool() - clientCAs.AppendCertsFromPEM(bytes) + clientCAs.AppendCertsFromPEM(caBytes) tlscfg.ClientCAs = clientCAs } @@ -71,16 +90,28 @@ func (cfg tlsServerCfg) Prepare() error { tlscfg.ClientAuth = tls.NoClientCert } + var pinnedFingerprints [][]byte + pinnedFingerprints, err = decodeFingerprints(cfg.PinnedClientCert) + if err != nil { + return fmt.Errorf("error decoding fingerprints: %s", err) + } + + if tlscfg.ClientAuth != tls.NoClientCert { + tlscfg.VerifyPeerCertificate = ReceptorVerifyFunc(tlscfg, pinnedFingerprints, + "", ExpectedHostnameTypeDNS, VerifyClient) + } + return MainInstance.SetServerTLSConfig(cfg.Name, tlscfg) } // tlsClientConfig stores the configuration options for a TLS client. type tlsClientConfig struct { - Name string `required:"true" description:"Name of this TLS client configuration"` - Cert string `required:"false" description:"Client certificate filename"` - Key string `required:"false" description:"Client private key filename"` - RootCAs string `required:"false" description:"Root CA bundle to use instead of system trust"` - InsecureSkipVerify bool `required:"false" description:"Accept any server cert" default:"false"` + Name string `required:"true" description:"Name of this TLS client configuration"` + Cert string `required:"false" description:"Client certificate filename"` + Key string `required:"false" description:"Client private key filename"` + RootCAs string `required:"false" description:"Root CA bundle to use instead of system trust"` + InsecureSkipVerify bool `required:"false" description:"Accept any server cert" default:"false"` + PinnedServerCert []string `required:"false" description:"Pinned fingerprint of required server certificate"` } // Prepare creates the tls.config and stores it in the global map. @@ -94,15 +125,15 @@ func (cfg tlsClientConfig) Prepare() error { if cfg.Cert == "" || cfg.Key == "" { return fmt.Errorf("cert and key must both be supplied or neither") } - certbytes, err := ioutil.ReadFile(cfg.Cert) + certBytes, err := ioutil.ReadFile(cfg.Cert) if err != nil { return err } - keybytes, err := ioutil.ReadFile(cfg.Key) + keyBytes, err := ioutil.ReadFile(cfg.Key) if err != nil { return err } - cert, err := tls.X509KeyPair(certbytes, keybytes) + cert, err := tls.X509KeyPair(certBytes, keyBytes) if err != nil { return err } @@ -110,19 +141,24 @@ func (cfg tlsClientConfig) Prepare() error { } if cfg.RootCAs != "" { - bytes, err := ioutil.ReadFile(cfg.RootCAs) + caBytes, err := ioutil.ReadFile(cfg.RootCAs) if err != nil { return fmt.Errorf("error reading root CAs file: %s", err) } rootCAs := x509.NewCertPool() - rootCAs.AppendCertsFromPEM(bytes) + rootCAs.AppendCertsFromPEM(caBytes) tlscfg.RootCAs = rootCAs } + pinnedFingerprints, err := decodeFingerprints(cfg.PinnedServerCert) + if err != nil { + return fmt.Errorf("error decoding fingerprints: %s", err) + } + tlscfg.InsecureSkipVerify = cfg.InsecureSkipVerify - return MainInstance.SetClientTLSConfig(cfg.Name, tlscfg) + return MainInstance.SetClientTLSConfig(cfg.Name, tlscfg, pinnedFingerprints) } func init() { diff --git a/pkg/pkg.go b/pkg/pkg.go deleted file mode 100644 index eddf2b573..000000000 --- a/pkg/pkg.go +++ /dev/null @@ -1,108 +0,0 @@ -package pkg - -import ( - "context" - "errors" - "fmt" - "os" - "strings" - - "github.com/ansible/receptor/pkg/backends" - "github.com/ansible/receptor/pkg/controlsvc" - "github.com/ansible/receptor/pkg/logger" - "github.com/ansible/receptor/pkg/netceptor" - "github.com/ansible/receptor/pkg/services" - "github.com/ansible/receptor/pkg/workceptor" -) - -// ErrNoBackends indicates that no backends were specified for a receptor instance. -var ErrNoBackends = errors.New("no backends were specified in serve config") - -// Receptor defines the configuration for a receptor instance. -type Receptor struct { - // Overrides the default loglevel on the root logger. - LogLevel *string `mapstructure:"log-level"` - // Enable receptor packet tracing. - EnableTracing bool `mapstructure:"enable-tracing"` - // Node ID. Defaults to local hostname. - ID *string `mapstructure:"id"` - // List of peer node-IDs to allow. - AllowedPeers []string `mapstructure:"allowed-peers"` - // Directory in which to store node data. - DataDir string `mapstructure:"data-dir"` - Backends *backends.Backends `mapstructure:"backends"` - Services *services.Services `mapstructure:"services"` - Workers *workceptor.Workers `mapstructure:"workers"` - Controllers *controlsvc.Controllers `mapstructure:"controllers"` -} - -// Serve launches an receptor instance and blocks until canceled or failed. -func (r Receptor) Serve(ctx context.Context) error { - logger.SetShowTrace(r.EnableTracing) - - if r.LogLevel != nil { - val, err := logger.GetLogLevelByName(*r.LogLevel) - if err != nil { - return fmt.Errorf("log level in serve config is invalid: %w", err) - } - logger.SetLogLevel(val) - } - - var id string - var err error - if r.ID == nil { - id, err = os.Hostname() - if err != nil { - return fmt.Errorf("node id is not set in serve config and could not get hostname: %w", err) - } - lchost := strings.ToLower(id) - if lchost == "localhost" || strings.HasPrefix(lchost, "localhost.") { - return fmt.Errorf("node id is not set in serve config and hostname is invalid (don't use localhost please): %w", err) - } - } else { - id = *r.ID - } - - nc := netceptor.New(ctx, id) - wc, err := workceptor.New(ctx, nc, r.DataDir) - if err != nil { - return fmt.Errorf("could not setup workceptor from serve config: %w", err) - } - - cv := controlsvc.New(true, nc) - - if r.Backends != nil { - if err := r.Backends.Setup(nc); err != nil { - return fmt.Errorf("could not setup listeners from serve config: %w", err) - } - } - - if r.Services != nil { - if err := r.Services.Setup(nc); err != nil { - return fmt.Errorf("could not setup services from serve config: %w", err) - } - } - - if r.Workers != nil { - if err := r.Workers.Setup(wc); err != nil { - return fmt.Errorf("could not setup workers from serve config: %w", err) - } - } - - if r.Controllers != nil { - if err := r.Controllers.Setup(ctx, cv); err != nil { - return fmt.Errorf("could not setup controllers from serve config: %w", err) - } - } - - // crutch to ensure refresh. - wc.ListKnownUnitIDs() - - if nc.BackendCount() < 1 { - return ErrNoBackends - } - - nc.BackendWait() - - return nil -} diff --git a/pkg/services/command.go b/pkg/services/command.go index 9f3ed6df0..efef84336 100644 --- a/pkg/services/command.go +++ b/pkg/services/command.go @@ -4,21 +4,23 @@ package services import ( - "fmt" + "crypto/tls" "net" "os/exec" - "strings" "github.com/ansible/receptor/pkg/logger" "github.com/ansible/receptor/pkg/netceptor" - "github.com/ansible/receptor/pkg/tls" "github.com/ansible/receptor/pkg/utils" "github.com/creack/pty" "github.com/ghjm/cmdline" + "github.com/google/shlex" ) func runCommand(qc net.Conn, command string) error { - args := strings.Split(command, " ") + args, err := shlex.Split(command) + if err != nil { + return err + } cmd := exec.Command(args[0], args[1:]...) tty, err := pty.Start(cmd) if err != nil { @@ -79,29 +81,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-command-service", "command-service", "Run an interactive command via a Receptor service", commandSvcCfg{}, cmdline.Section(servicesSection)) } - -// Command executes a command on a connection. -type Command struct { - // Receptor service name to bind to. - Service string `mapstructure:"service"` - // Command to execute on a connection. - Command string `mapstructure:"command"` - // TLS config to use for the transport within receptor. - // Leave empty for no TLS. - TLS *tls.ServerConf `mapstructure:"tls"` -} - -func (s *Command) setup(nc *netceptor.Netceptor) error { - var t *tls.Config - var err error - if s.TLS != nil { - t, err = s.TLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls config for command service %s: %w", s.Service, err) - } - } - - go CommandService(nc, s.Service, t, s.Command) - - return nil -} diff --git a/pkg/services/ip_router.go b/pkg/services/ip_router.go index 883e4df06..bd32a5ae6 100644 --- a/pkg/services/ip_router.go +++ b/pkg/services/ip_router.go @@ -47,7 +47,8 @@ type IPRouterService struct { // NewIPRouter creates a new IP router service. func NewIPRouter(nc *netceptor.Netceptor, networkName string, tunInterface string, - localNet string, routes string) (*IPRouterService, error) { + localNet string, routes string, +) (*IPRouterService, error) { ipr := &IPRouterService{ nc: nc, networkName: networkName, @@ -382,21 +383,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-ip-router", "ip-router", "Run an IP router using a tun interface", ipRouterCfg{}, cmdline.Section(servicesSection)) } - -// IPRouter routes IP packages through receptor. -type IPRouter struct { - // Name of this network and service. - NetworkName string `mapstructure:"network-name"` - // Name of the local tun interface. - Interface string `mapstructure:"interface"` - // Local /30 CIDR address. - LocalNet string `mapstructure:"local-net"` - // Comma separated list of CIDR subnets to advertise. - Routes string `mapstructure:"routes"` -} - -func (s *IPRouter) setup(nc *netceptor.Netceptor) error { - _, err := NewIPRouter(nc, s.NetworkName, s.Interface, s.LocalNet, s.Routes) - - return err -} diff --git a/pkg/services/services.go b/pkg/services/services.go deleted file mode 100644 index c19448cea..000000000 --- a/pkg/services/services.go +++ /dev/null @@ -1,99 +0,0 @@ -//go:build linux && !no_ip_router && linux && !no_services -// +build linux,!no_ip_router,linux,!no_services - -package services - -import ( - "fmt" - - "github.com/ansible/receptor/pkg/netceptor" -) - -// Services defines a set of receptor services. -type Services struct { - // Run commands. - Command []Command `mapstructure:"command"` - // Route IP. - IPRouter []IPRouter `mapstructure:"ip-router"` - // Proxy sockets. - Proxies *Proxies `mapstructure:"proxies"` -} - -// Services defines a set of receptor services that proxy sockets. -type Proxies struct { - // Expose TCP ports. - TCPIn []TCPInProxy `mapstructure:"tcp-in"` - // Export TCP ports. - TCPOut []TCPOutProxy `mapstructure:"tcp-out"` - // Expose UDP ports. - UDPIn []UDPInProxy `mapstructure:"udp-in"` - // Export udp sockets. - UDPOut []UDPOutProxy `mapstructure:"udp-out"` - // Expose unix sockets. - UnixIn []UnixInProxy `mapstructure:"unix-in"` - // Export unix sockets. - UnixOut []UnixOutProxy `mapstructure:"unix-out"` -} - -func (p Proxies) setup(nc *netceptor.Netceptor) error { - for _, c := range p.UnixIn { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup unix inbound proxy connection from proxies config: %w", err) - } - } - - for _, c := range p.UnixOut { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup unix outbound proxy connection from proxies config: %w", err) - } - } - - for _, c := range p.UDPIn { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup udp inbound proxy connection from proxies config: %w", err) - } - } - - for _, c := range p.UDPOut { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup udp outbound proxy connection from proxies config: %w", err) - } - } - - for _, c := range p.TCPIn { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup tcp inbound proxy connection from proxies config: %w", err) - } - } - - for _, c := range p.TCPOut { - if err := c.setup(nc); err != nil { - return fmt.Errorf("could not setup tcp outbound proxy connection from proxies config: %w", err) - } - } - - return nil -} - -// Setup attaches all the defined services to the given netceptor. -func (s *Services) Setup(nc *netceptor.Netceptor) error { - for _, s := range s.Command { - if err := s.setup(nc); err != nil { - return fmt.Errorf("could not setup control service from service config: %w", err) - } - } - - for _, r := range s.IPRouter { - if err := r.setup(nc); err != nil { - return fmt.Errorf("could not setup ip router from service config: %w", err) - } - } - - if s.Proxies != nil { - if err := s.Proxies.setup(nc); err != nil { - return fmt.Errorf("could not setup proxies from service config: %w", err) - } - } - - return nil -} diff --git a/pkg/services/tcp_proxy.go b/pkg/services/tcp_proxy.go index 3f756e336..53c225c7f 100644 --- a/pkg/services/tcp_proxy.go +++ b/pkg/services/tcp_proxy.go @@ -4,20 +4,21 @@ package services import ( + "crypto/tls" "fmt" "net" "strconv" "github.com/ansible/receptor/pkg/logger" "github.com/ansible/receptor/pkg/netceptor" - "github.com/ansible/receptor/pkg/tls" "github.com/ansible/receptor/pkg/utils" "github.com/ghjm/cmdline" ) // TCPProxyServiceInbound listens on a TCP port and forwards the connection over the Receptor network. func TCPProxyServiceInbound(s *netceptor.Netceptor, host string, port int, tlsServer *tls.Config, - node string, rservice string, tlsClient *tls.Config) error { + node string, rservice string, tlsClient *tls.Config, +) error { tli, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) if tlsServer != nil { tli = tls.NewListener(tli, tlsServer) @@ -48,7 +49,8 @@ func TCPProxyServiceInbound(s *netceptor.Netceptor, host string, port int, tlsSe // TCPProxyServiceOutbound listens on the Receptor network and forwards the connection via TCP. func TCPProxyServiceOutbound(s *netceptor.Netceptor, service string, tlsServer *tls.Config, - address string, tlsClient *tls.Config) error { + address string, tlsClient *tls.Config, +) error { qli, err := s.ListenAndAdvertise(service, tlsServer, map[string]string{ "type": "TCP Proxy", "address": address, @@ -95,7 +97,7 @@ type tcpProxyInboundCfg struct { // Run runs the action. func (cfg tcpProxyInboundCfg) Run() error { logger.Debug("Running TCP inbound proxy service %v\n", cfg) - tlsClientCfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLSClient, cfg.RemoteNode, "receptor") + tlsClientCfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLSClient, cfg.RemoteNode, netceptor.ExpectedHostnameTypeReceptor) if err != nil { return err } @@ -127,7 +129,7 @@ func (cfg tcpProxyOutboundCfg) Run() error { if err != nil { return err } - tlsClientCfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLSClient, host, "dns") + tlsClientCfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLSClient, host, netceptor.ExpectedHostnameTypeDNS) if err != nil { return err } @@ -141,87 +143,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-proxies", "tcp-client", "Listen on a Receptor service and forward via TCP", tcpProxyOutboundCfg{}, cmdline.Section(servicesSection)) } - -// TCPInProxy exposes an exported tcp port. -type TCPInProxy struct { - // Receptor service name to connect to. - RemoteService string `mapstructure:"remote-service"` - // Receptor node to connect to. - RemoteNode string `mapstructure:"remote-node"` - // Address to listen on ("host:port" from net package). - Address string `mapstructure:"address"` - // TLS client config for the TCP connection. - // Leave empty for no TLS. - PortTLS *tls.ClientConf `mapstructure:"port-tls"` - // TLS config to use for the transport within receptor. - // Leave empty for no TLS. - MeshTLS *tls.ServerConf `mapstructure:"mesh-tls"` -} - -func (t TCPInProxy) setup(nc *netceptor.Netceptor) error { - var err error - var tClient, tServer *tls.Config - if t.PortTLS != nil { - tClient, err = t.PortTLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls client config for tls inbound proxy %s: %w", t.Address, err) - } - } - if t.MeshTLS != nil { - tServer, err = t.MeshTLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls server config for tls inbound proxy %s: %w", t.Address, err) - } - } - host, port, err := net.SplitHostPort(t.Address) - if err != nil { - return fmt.Errorf("address %s for tls inbound proxy is invalid: %w", t.Address, err) - } - i, err := strconv.Atoi(port) - if err != nil { - return fmt.Errorf("address %s for tls inbound proxy contains invalid port: %w", t.Address, err) - } - - return TCPProxyServiceInbound( - nc, - host, - i, - tServer, - t.RemoteNode, - t.RemoteService, - tClient, - ) -} - -// TCPOutProxy exports a local tcp port. -type TCPOutProxy struct { - // Receptor service name to bind to. - Service string `mapstructure:"service"` - // Address for outbound TCP connection. - Address string `mapstructure:"address"` - // TLS client config for the TCP connection. - // Leave empty for no TLS. - PortTLS *tls.ClientConf `mapstructure:"port-tls"` - // TLS config to use for the transport within receptor. - // Leave empty for no TLS. - MeshTLS *tls.ServerConf `mapstructure:"mesh-tls"` -} - -func (t TCPOutProxy) setup(nc *netceptor.Netceptor) error { - var err error - var tClient, tServer *tls.Config - if t.PortTLS != nil { - tClient, err = t.PortTLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls client config for tls outbound proxy %s: %w", t.Address, err) - } - } - if t.MeshTLS != nil { - tServer, err = t.MeshTLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls server config for tls outbound proxy %s: %w", t.Address, err) - } - } - - return TCPProxyServiceOutbound(nc, t.Service, tServer, t.Address, tClient) -} diff --git a/pkg/services/udp_proxy.go b/pkg/services/udp_proxy.go index a772877ad..ef528209f 100644 --- a/pkg/services/udp_proxy.go +++ b/pkg/services/udp_proxy.go @@ -6,7 +6,6 @@ package services import ( "fmt" "net" - "strconv" "github.com/ansible/receptor/pkg/logger" "github.com/ansible/receptor/pkg/netceptor" @@ -208,37 +207,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-proxies", "udp-client", "Listen on a Receptor service and forward via UDP", udpProxyOutboundCfg{}, cmdline.Section(servicesSection)) } - -// UDPInProxy exposes an exported udp port. -type UDPInProxy struct { - Address string `mapstructure:"address"` - // Receptor node to connect to. - RemoteNode string `mapstructure:"remote-node"` - // Receptor service name to connect to. - RemoteService string `mapstructure:"remote-service"` -} - -func (p *UDPInProxy) setup(nc *netceptor.Netceptor) error { - host, port, err := net.SplitHostPort(p.Address) - if err != nil { - return fmt.Errorf("address %s for udp inbound proxy is invalid: %w", p.Address, err) - } - i, err := strconv.Atoi(port) - if err != nil { - return fmt.Errorf("address %s for udp inbound proxy contains invalid port: %w", p.Address, err) - } - - return UDPProxyServiceInbound(nc, host, i, p.RemoteNode, p.RemoteService) -} - -// UDPOutProxy exports a local unix socket. -type UDPOutProxy struct { - // Receptor service name to bind to. - Service string `mapstructure:"service"` - // Address for outbound UDP connection. - Address string `mapstructure:"address"` -} - -func (p *UDPOutProxy) setup(nc *netceptor.Netceptor) error { - return UDPProxyServiceOutbound(nc, p.Service, p.Address) -} diff --git a/pkg/services/unix_proxy.go b/pkg/services/unix_proxy.go index 53dba6c4a..7964b7f24 100644 --- a/pkg/services/unix_proxy.go +++ b/pkg/services/unix_proxy.go @@ -4,6 +4,7 @@ package services import ( + "crypto/tls" "fmt" "net" "os" @@ -11,14 +12,14 @@ import ( "github.com/ansible/receptor/pkg/logger" "github.com/ansible/receptor/pkg/netceptor" - "github.com/ansible/receptor/pkg/tls" "github.com/ansible/receptor/pkg/utils" "github.com/ghjm/cmdline" ) // UnixProxyServiceInbound listens on a Unix socket and forwards connections over the Receptor network. func UnixProxyServiceInbound(s *netceptor.Netceptor, filename string, permissions os.FileMode, - node string, rservice string, tlscfg *tls.Config) error { + node string, rservice string, tlscfg *tls.Config, +) error { uli, lock, err := utils.UnixSocketListen(filename, permissions) if err != nil { return fmt.Errorf("error opening Unix socket: %s", err) @@ -89,7 +90,7 @@ type unixProxyInboundCfg struct { // Run runs the action. func (cfg unixProxyInboundCfg) Run() error { logger.Debug("Running Unix socket inbound proxy service %v\n", cfg) - tlscfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLS, cfg.RemoteNode, "receptor") + tlscfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLS, cfg.RemoteNode, netceptor.ExpectedHostnameTypeReceptor) if err != nil { return err } @@ -124,59 +125,3 @@ func init() { "unix-socket-client", "Listen via Receptor and forward to a Unix socket", unixProxyOutboundCfg{}, cmdline.Section(servicesSection)) } } - -// UnixInProxy exposes an exported unix socket. -type UnixInProxy struct { - // Socket filename, which will be overwritten. - File string `mapstructure:"file"` - // Socket file permissions. - Permissions *int `mapstructure:"permissions"` - // Receptor node to connect to. - RemoteNode string `mapstructure:"remote-node"` - // Receptor service name to connect to. - RemoteService string `mapstructure:"remote-service"` - // TLS config to use for the transport within receptor. - // Leave empty for no TLS. - TLS tls.ClientConf `mapstructure:"tls"` -} - -func (p *UnixInProxy) setup(nc *netceptor.Netceptor) error { - perms := 0o600 - if p.Permissions != nil { - perms = *p.Permissions - } - - t, err := p.TLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls config for unix inbound proxy %s: %w", p.File, err) - } - - return UnixProxyServiceInbound( - nc, - p.File, - os.FileMode(perms), - p.RemoteNode, - p.RemoteService, - t, - ) -} - -// UnixOutProxy exports a local unix socket. -type UnixOutProxy struct { - // Receptor service name to bind to. - Service string `mapstructure:"service"` - // Socket filename, which must already exist. - File string `mapstructure:"file"` - // TLS config to use for the transport within receptor. - // Leave empty for no TLS. - TLS tls.ServerConf `mapstructure:"tls"` -} - -func (p *UnixOutProxy) setup(nc *netceptor.Netceptor) error { - t, err := p.TLS.TLSConfig() - if err != nil { - return fmt.Errorf("could not create tls config for unix outbound proxy %s: %w", p.File, err) - } - - return UnixProxyServiceOutbound(nc, p.Service, t, p.File) -} diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go deleted file mode 100644 index 36b7ab5b4..000000000 --- a/pkg/tls/tls.go +++ /dev/null @@ -1,117 +0,0 @@ -package tls - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "io/ioutil" -) - -type ServerConf struct { - // Path to file containing certificate. - Cert string `mapstructure:"cert"` - // Path to file containing key. - Key string `mapstructure:"key"` - // Path to file containing CA for client validation. - CA string `mapstructure:"ca"` - // Do not verify clients. - SkipVerify bool `mapstructure:"insecure-no-verify"` -} - -func (c ServerConf) TLSConfig() (*tls.Config, error) { - tlscfg := &tls.Config{ - ClientAuth: tls.RequireAndVerifyClientCert, - } - - if c.SkipVerify { - tlscfg.ClientAuth = tls.NoClientCert - } else { - bytes, err := ioutil.ReadFile(c.CA) - if err != nil { - return nil, fmt.Errorf("error reading client CAs file: %s", err) - } - clientCAs := x509.NewCertPool() - clientCAs.AppendCertsFromPEM(bytes) - tlscfg.ClientCAs = clientCAs - } - - certbytes, err := ioutil.ReadFile(c.Cert) - if err != nil { - return nil, err - } - keybytes, err := ioutil.ReadFile(c.Key) - if err != nil { - return nil, err - } - cert, err := tls.X509KeyPair(certbytes, keybytes) - if err != nil { - return nil, err - } - - tlscfg.Certificates = []tls.Certificate{cert} - - return tlscfg, nil -} - -type ClientConf struct { - // Path to file containing certificate. - Cert string `mapstructure:"cert"` - // Path to file containing key. - Key string `mapstructure:"key"` - // Path to file containing CA for server validation. - CA string `mapstructure:"ca"` - // Do not verify server. - SkipVerify bool `mapstructure:"insecure-no-verify"` -} - -func (c ClientConf) TLSConfig() (*tls.Config, error) { - tlscfg := &tls.Config{} - - if c.SkipVerify { - tlscfg.InsecureSkipVerify = true - } else { - bytes, err := ioutil.ReadFile(c.CA) - if err != nil { - return nil, fmt.Errorf("error reading root CAs file: %s", err) - } - - rootCAs := x509.NewCertPool() - rootCAs.AppendCertsFromPEM(bytes) - tlscfg.RootCAs = rootCAs - } - - if (c.Cert == "") != (c.Key == "") { - return nil, fmt.Errorf("cert and key must both be supplied or neither") - } - - if c.Cert != "" { - certbytes, err := ioutil.ReadFile(c.Cert) - if err != nil { - return nil, err - } - keybytes, err := ioutil.ReadFile(c.Key) - if err != nil { - return nil, err - } - cert, err := tls.X509KeyPair(certbytes, keybytes) - if err != nil { - return nil, err - } - tlscfg.Certificates = []tls.Certificate{cert} - } - - return tlscfg, nil -} - -// Alias some contents of crypto/tls to avoid import madness. - -type ( - Config = tls.Config - Conn = tls.Conn -) - -var ( - NewListener = tls.NewListener - Dial = tls.Dial - DialWithDialer = tls.DialWithDialer -) diff --git a/pkg/utils/broker.go b/pkg/utils/broker.go index b4bf6630f..bae858ae9 100644 --- a/pkg/utils/broker.go +++ b/pkg/utils/broker.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "sync" ) // Broker code adapted from https://stackoverflow.com/questions/36417199/how-to-broadcast-message-using-channel @@ -23,7 +24,7 @@ func NewBroker(ctx context.Context, msgType reflect.Type) *Broker { b := &Broker{ ctx: ctx, msgType: msgType, - publishCh: make(chan interface{}, 1), + publishCh: make(chan interface{}), subCh: make(chan chan interface{}), unsubCh: make(chan chan interface{}), } @@ -47,40 +48,41 @@ func (b *Broker) start() { subs[msgCh] = struct{}{} case msgCh := <-b.unsubCh: delete(subs, msgCh) + close(msgCh) case msg := <-b.publishCh: + wg := sync.WaitGroup{} for msgCh := range subs { + wg.Add(1) go func(msgCh chan interface{}) { + defer wg.Done() select { case msgCh <- msg: case <-b.ctx.Done(): } }(msgCh) } + wg.Wait() } } } // Subscribe registers to receive messages from the broker. func (b *Broker) Subscribe() chan interface{} { - if b == nil || b.ctx == nil { - fmt.Printf("foo\n") - } - if b.ctx.Err() == nil { - msgCh := make(chan interface{}, 1) - b.subCh <- msgCh - + msgCh := make(chan interface{}) + select { + case <-b.ctx.Done(): + return nil + case b.subCh <- msgCh: return msgCh } - - return nil } // Unsubscribe de-registers a message receiver. func (b *Broker) Unsubscribe(msgCh chan interface{}) { - if b.ctx.Err() == nil { - b.unsubCh <- msgCh + select { + case <-b.ctx.Done(): + case b.unsubCh <- msgCh: } - close(msgCh) } // Publish sends a message to all subscribers. @@ -88,8 +90,9 @@ func (b *Broker) Publish(msg interface{}) error { if reflect.TypeOf(msg) != b.msgType { return fmt.Errorf("messages to broker must be of type %s", b.msgType.String()) } - if b.ctx.Err() == nil { - b.publishCh <- msg + select { + case <-b.ctx.Done(): + case b.publishCh <- msg: } return nil diff --git a/pkg/utils/cancel_with_err_context.go b/pkg/utils/cancel_with_err_context.go deleted file mode 100644 index 6259db260..000000000 --- a/pkg/utils/cancel_with_err_context.go +++ /dev/null @@ -1,82 +0,0 @@ -package utils - -import ( - "context" - "sync" - "time" -) - -// CancelWithErrFunc is like a regular context.CancelFunc, but you can specify an error to return. -type CancelWithErrFunc func(err error) - -// CancelWithErrContext is a context that can be cancelled with a specific error return. -type CancelWithErrContext struct { - parentCtx context.Context - errChan chan error - doneChan chan struct{} - closeOnce sync.Once - err error -} - -// ContextWithCancelWithErr returns a context and a CancelWithErrFunc. This functions like a normal -// context cancel function, except you can specify what error should be returned. -func ContextWithCancelWithErr(parent context.Context) (*CancelWithErrContext, CancelWithErrFunc) { - cwe := &CancelWithErrContext{ - parentCtx: parent, - errChan: make(chan error), - doneChan: make(chan struct{}), - closeOnce: sync.Once{}, - } - go func() { - for { - select { - case <-parent.Done(): - cwe.closeDoneChan() - - return - case err := <-cwe.errChan: - cwe.err = err - if err != nil { - cwe.closeDoneChan() - - return - } - } - } - }() - - return cwe, func(err error) { - cwe.err = err - cwe.closeDoneChan() - } -} - -func (cwe *CancelWithErrContext) closeDoneChan() { - cwe.closeOnce.Do(func() { - close(cwe.doneChan) - }) -} - -// Done implements Context.Done(). -func (cwe *CancelWithErrContext) Done() <-chan struct{} { - return cwe.doneChan -} - -// Err implements Context.Err(). -func (cwe *CancelWithErrContext) Err() error { - if cwe.err != nil { - return cwe.err - } - - return cwe.parentCtx.Err() -} - -// Deadline implements Context.Deadline(). -func (cwe *CancelWithErrContext) Deadline() (time time.Time, ok bool) { - return cwe.parentCtx.Deadline() -} - -// Value implements Context.Value(). -func (cwe *CancelWithErrContext) Value(key interface{}) interface{} { - return cwe.parentCtx.Value(key) -} diff --git a/pkg/utils/flock.go b/pkg/utils/flock.go index ed2871dcb..6e004f0e6 100644 --- a/pkg/utils/flock.go +++ b/pkg/utils/flock.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package utils diff --git a/pkg/utils/flock_windows.go b/pkg/utils/flock_windows.go index aa110c79d..371ef3606 100644 --- a/pkg/utils/flock_windows.go +++ b/pkg/utils/flock_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package utils diff --git a/pkg/utils/readstring_context.go b/pkg/utils/readstring_context.go index f839d496a..1b2f98f53 100644 --- a/pkg/utils/readstring_context.go +++ b/pkg/utils/readstring_context.go @@ -15,7 +15,7 @@ type readStringResult = struct { // important for callers to error out of further use of the bufio. Also, the goroutine will not // exit until the bufio's underlying connection is closed. func ReadStringContext(ctx context.Context, reader *bufio.Reader, delim byte) (string, error) { - result := make(chan *readStringResult) + result := make(chan *readStringResult, 1) go func() { str, err := reader.ReadString(delim) result <- &readStringResult{ diff --git a/pkg/utils/unixsock.go b/pkg/utils/unixsock.go index 0ba6be864..6b6fa39cb 100644 --- a/pkg/utils/unixsock.go +++ b/pkg/utils/unixsock.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package utils diff --git a/pkg/utils/unixsock_windows.go b/pkg/utils/unixsock_windows.go index 2476c950c..9021746f0 100644 --- a/pkg/utils/unixsock_windows.go +++ b/pkg/utils/unixsock_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package utils diff --git a/pkg/workceptor/cmdline.go b/pkg/workceptor/cmdline.go index faf4102c7..caaed11e5 100644 --- a/pkg/workceptor/cmdline.go +++ b/pkg/workceptor/cmdline.go @@ -1,3 +1,4 @@ +//go:build !no_workceptor // +build !no_workceptor package workceptor diff --git a/pkg/workceptor/command.go b/pkg/workceptor/command.go index d42b571a0..f245885eb 100644 --- a/pkg/workceptor/command.go +++ b/pkg/workceptor/command.go @@ -95,6 +95,7 @@ func commandRunner(command string, params string, unitdir string) error { } doneChan := make(chan bool, 1) go cmdWaiter(cmd, doneChan) + writeStatusFailures := 0 loop: for { select { @@ -111,6 +112,13 @@ loop: err = status.UpdateBasicStatus(statusFilename, WorkStateRunning, fmt.Sprintf("Running: PID %d", cmd.Process.Pid), stdoutSize(unitdir)) if err != nil { logger.Error("Error updating status file %s: %s", statusFilename, err) + writeStatusFailures++ + if writeStatusFailures > 3 { + logger.Error("Exceeded retries for updating status file %s: %s", statusFilename, err) + os.Exit(-1) + } + } else { + writeStatusFailures = 0 } } } @@ -221,7 +229,9 @@ func (cw *commandUnit) Start() error { level := logger.GetLogLevel() levelName, _ := logger.LogLevelToName(level) cw.UpdateBasicStatus(WorkStatePending, "Launching command runner", 0) - cmd := exec.Command(os.Args[0], "--log-level", levelName, "--command-runner", + cmd := exec.Command(os.Args[0], "--node", "id=worker", + "--log-level", levelName, + "--command-runner", fmt.Sprintf("command=%s", cw.command), fmt.Sprintf("params=%s", cw.Status().ExtraData.(*commandExtraData).Params), fmt.Sprintf("unitdir=%s", cw.UnitDir())) @@ -407,31 +417,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-workers", "command-runner", "Wrapper around a process invocation", commandRunnerCfg{}, cmdline.Hidden) } - -// Command runs a process. -type Command struct { - // Name for this worker type. - WorkType string `mapstructure:"work-type"` - // Command to run to process units of work. - Command string `mapstructure:"command"` - // Command-line parameters. - Params string `mapstructure:"parameters"` - // Allow users to add more parameters. - AllowRuntimeParams bool `mapstructure:"allow-runtime-parameters"` -} - -func (c Command) setup(wc *Workceptor) error { - factory := func(w *Workceptor, unitID string, workType string) WorkUnit { - cw := &commandUnit{ - BaseWorkUnit: BaseWorkUnit{status: StatusFileData{ExtraData: &commandExtraData{}}}, - command: c.Command, - baseParams: c.Params, - allowRuntimeParams: c.AllowRuntimeParams, - } - cw.BaseWorkUnit.Init(w, unitID, workType) - - return cw - } - - return wc.RegisterWorker(c.WorkType, factory, false) -} diff --git a/pkg/workceptor/command_detach_unixlike.go b/pkg/workceptor/command_detach_unixlike.go index 873d4e4e5..0cba1cf44 100644 --- a/pkg/workceptor/command_detach_unixlike.go +++ b/pkg/workceptor/command_detach_unixlike.go @@ -1,3 +1,4 @@ +//go:build !windows && !no_workceptor // +build !windows,!no_workceptor package workceptor diff --git a/pkg/workceptor/command_detach_windows.go b/pkg/workceptor/command_detach_windows.go index 004c2ca12..c03148c18 100644 --- a/pkg/workceptor/command_detach_windows.go +++ b/pkg/workceptor/command_detach_windows.go @@ -1,3 +1,4 @@ +//go:build windows && !no_workceptor // +build windows,!no_workceptor package workceptor diff --git a/pkg/workceptor/controlsvc.go b/pkg/workceptor/controlsvc.go index 167794c36..34fc6d185 100644 --- a/pkg/workceptor/controlsvc.go +++ b/pkg/workceptor/controlsvc.go @@ -4,6 +4,7 @@ package workceptor import ( + "context" "fmt" "os" "path" @@ -133,7 +134,7 @@ func boolFromMap(config map[string]interface{}, name string) (bool, error) { return false, nil } - return false, fmt.Errorf("field %s value %s is not convertible to an bool", name, value) + return false, fmt.Errorf("field %s value %s is not convertible to a bool", name, value) } func (t *workceptorCommandType) InitFromJSON(config map[string]interface{}) (controlsvc.ControlCommand, error) { @@ -195,8 +196,8 @@ func (t *workceptorCommandType) InitFromJSON(config map[string]interface{}) (con return c, nil } -func (c *workceptorCommand) processSignature(workType, signature string, connIsUnix bool) error { - shouldVerifySignature := c.w.ShouldVerifySignature(workType) +func (c *workceptorCommand) processSignature(workType, signature string, connIsUnix, signWork bool) error { + shouldVerifySignature := c.w.ShouldVerifySignature(workType, signWork) if !shouldVerifySignature && signature != "" { return fmt.Errorf("work type did not expect a signature") } @@ -210,8 +211,17 @@ func (c *workceptorCommand) processSignature(workType, signature string, connIsU return nil } +func getSignWorkFromStatus(status *StatusFileData) bool { + red, ok := status.ExtraData.(*remoteExtraData) + if ok { + return red.SignWork + } + + return false +} + // Worker function called by the control service to process a "work" command. -func (c *workceptorCommand) ControlFunc(nc *netceptor.Netceptor, cfo controlsvc.ControlFuncOperations) (map[string]interface{}, error) { +func (c *workceptorCommand) ControlFunc(ctx context.Context, nc *netceptor.Netceptor, cfo controlsvc.ControlFuncOperations) (map[string]interface{}, error) { addr := cfo.RemoteAddr() connIsUnix := false if addr.Network() == "unix" { @@ -264,7 +274,7 @@ func (c *workceptorCommand) ControlFunc(nc *netceptor.Netceptor, cfo controlsvc. } workParams[k] = vStr } - err = c.processSignature(workType, signature, connIsUnix) + err = c.processSignature(workType, signature, connIsUnix, signWork) if err != nil { return nil, err } @@ -370,7 +380,8 @@ func (c *workceptorCommand) ControlFunc(nc *netceptor.Netceptor, cfo controlsvc. return cfr, err } status := unit.Status() - err = c.processSignature(status.WorkType, signature, connIsUnix) + signWork := getSignWorkFromStatus(status) + err = c.processSignature(status.WorkType, signature, connIsUnix, signWork) if err != nil { return nil, err } @@ -407,15 +418,13 @@ func (c *workceptorCommand) ControlFunc(nc *netceptor.Netceptor, cfo controlsvc. return nil, err } status := unit.Status() - err = c.processSignature(status.WorkType, signature, connIsUnix) + signWork := getSignWorkFromStatus(status) + err = c.processSignature(status.WorkType, signature, connIsUnix, signWork) if err != nil { return nil, err } - doneChan := make(chan struct{}) - defer func() { - close(doneChan) - }() - resultChan, err := c.w.GetResults(unitid, startPos, doneChan) + + resultChan, err := c.w.GetResults(ctx, unitid, startPos) if err != nil { return nil, err } @@ -423,6 +432,7 @@ func (c *workceptorCommand) ControlFunc(nc *netceptor.Netceptor, cfo controlsvc. if err != nil { return nil, err } + err = cfo.Close() if err != nil { return nil, err diff --git a/pkg/workceptor/kubernetes.go b/pkg/workceptor/kubernetes.go index 4eb273538..8cb995e5e 100644 --- a/pkg/workceptor/kubernetes.go +++ b/pkg/workceptor/kubernetes.go @@ -5,6 +5,7 @@ package workceptor import ( "context" + "errors" "fmt" "io" "io/ioutil" @@ -18,7 +19,7 @@ import ( "github.com/ghjm/cmdline" "github.com/google/shlex" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" @@ -65,31 +66,50 @@ type kubeExtraData struct { // ErrPodCompleted is returned when pod has already completed before we could attach. var ErrPodCompleted = fmt.Errorf("pod ran to completion") +// ErrImagePullBackOff is returned when the image for the container in the Pod cannot be pulled. +var ErrImagePullBackOff = fmt.Errorf("container failed to start") + // podRunningAndReady is a completion criterion for pod ready to be attached to. -func podRunningAndReady(event watch.Event) (bool, error) { - if event.Type == watch.Deleted { - return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") - } - - if t, ok := event.Object.(*corev1.Pod); ok { - switch t.Status.Phase { - case corev1.PodFailed, corev1.PodSucceeded: - return false, ErrPodCompleted - case corev1.PodRunning: - conditions := t.Status.Conditions - if conditions == nil { - return false, nil - } - for i := range conditions { - if conditions[i].Type == corev1.PodReady && - conditions[i].Status == corev1.ConditionTrue { - return true, nil +func podRunningAndReady() func(event watch.Event) (bool, error) { + imagePullBackOffRetries := 3 + inner := func(event watch.Event) (bool, error) { + if event.Type == watch.Deleted { + return false, apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") + } + if t, ok := event.Object.(*corev1.Pod); ok { + switch t.Status.Phase { + case corev1.PodFailed, corev1.PodSucceeded: + return false, ErrPodCompleted + case corev1.PodRunning, corev1.PodPending: + conditions := t.Status.Conditions + if conditions == nil { + return false, nil + } + for i := range conditions { + if conditions[i].Type == corev1.PodReady && + conditions[i].Status == corev1.ConditionTrue { + return true, nil + } + if conditions[i].Type == corev1.ContainersReady && + conditions[i].Status == corev1.ConditionFalse { + statuses := t.Status.ContainerStatuses + for j := range statuses { + if statuses[j].State.Waiting.Reason == "ImagePullBackOff" { + if imagePullBackOffRetries == 0 { + return false, ErrImagePullBackOff + } + imagePullBackOffRetries-- + } + } + } } } } + + return false, nil } - return false, nil + return inner } func (kw *kubeUnit) createPod(env map[string]string) error { @@ -208,7 +228,10 @@ func (kw *kubeUnit) createPod(env map[string]string) error { if kw.podPendingTimeout != time.Duration(0) { ctxPodReady, _ = context.WithTimeout(kw.ctx, kw.podPendingTimeout) } - ev, err := watch2.UntilWithSync(ctxPodReady, lw, &corev1.Pod{}, nil, podRunningAndReady) + ev, err := watch2.UntilWithSync(ctxPodReady, lw, &corev1.Pod{}, nil, podRunningAndReady()) + if ev == nil || ev.Object == nil { + return fmt.Errorf("did not return an event while watching pod for work unit %s", kw.ID()) + } var ok bool kw.pod, ok = ev.Object.(*corev1.Pod) if !ok { @@ -234,9 +257,6 @@ func (kw *kubeUnit) createPod(env map[string]string) error { return err } - if ev == nil { - return fmt.Errorf("pod disappeared during watch") - } return nil } @@ -319,7 +339,9 @@ func (kw *kubeUnit) runWorkUsingLogger() { var stdin *stdinReader if !skipStdin { stdin, err = newStdinReader(kw.UnitDir()) - if err != nil { + if errors.Is(err, errFileSizeZero) { + skipStdin = true + } else if err != nil { errMsg := fmt.Sprintf("Error opening stdin file: %s", err) logger.Error(errMsg) kw.UpdateBasicStatus(WorkStateFailed, errMsg, 0) @@ -361,6 +383,8 @@ func (kw *kubeUnit) runWorkUsingLogger() { } } }() + } else { + kw.UpdateBasicStatus(WorkStateRunning, "Pod Running", stdout.Size()) } // Actually run the streams. This blocks until the pod finishes. @@ -673,7 +697,6 @@ func readFileToString(filename string) (string, error) { } // SetFromParams sets the in-memory state from parameters. -//nolint:ifshort // Method to magical for linter func (kw *kubeUnit) SetFromParams(params map[string]string) error { ked := kw.status.ExtraData.(*kubeExtraData) type value struct { @@ -962,106 +985,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-workers", "work-kubernetes", "Run a worker using Kubernetes", workKubeCfg{}, cmdline.Section(workersSection)) } - -// Kubernetes allows receptor interfacing with a k8s cluster. -type Kubernetes struct { - // Name for this worker type. - WorkType string `mapstructure:"work-type"` - // Kubernetes namespace to create pods in. - Namespace *string `mapstructure:"namespace"` - // Container image to use for the worker pod. - Image string `mapstructure:"image"` - // Command to run in the container (overrides entrypoint). - Command string `mapstructure:"command"` - // Command-line parameters to pass to the entrypoint. - Params string `mapstructure:"parameters"` - // One of: kubeconfig, incluster. - AuthMethod *string `mapstructure:"auth-method"` // default:"incluster"` - // Kubeconfig filename (for authmethod=kubeconfig). - KubeConfig *string `mapstructure:"kube-config"` - // Pod definition filename, in json or yaml format. - Pod string `mapstructure:"pod"` - // Allow passing API parameters at runtime. - AllowRuntimeAuth bool `mapstructure:"allow-runtime-auth"` - // Allow specifying image & command at runtime. - AllowRuntimeCommand bool `mapstructure:"allow-runtime-command"` - // Allow adding command parameters at runtime. - AllowRuntimeParams bool `mapstructure:"allow-runtime-parameters"` - // Allow passing Pod at runtime. - AllowRuntimePod bool `mapstructure:"allow-runtime-pod"` - // On restart, keep the pod if in pending state instead of deleting it. - KeepPodOnRestart bool `mapstructure:"keep-pod-on-restart"` - // Method for connecting to worker pods: logger or tcp. - StreamMethod *string `mapstructure:"stream-method"` -} - -func (k Kubernetes) setup(wc *Workceptor) error { - authMethod := "incluster" - if k.AuthMethod != nil { - authMethod = *k.AuthMethod - } - if authMethod != "kubeconfig" && authMethod != "incluster" && authMethod != "runtime" { - return fmt.Errorf("invalid AuthMethod: %s", authMethod) - } - namespace := "" - if k.Namespace != nil { - namespace = *k.Namespace - } else if !(authMethod == "kubeconfig" || k.AllowRuntimeAuth) { - return fmt.Errorf("must provide namespace when AuthMethod is not kubeconfig") - } - kubeConfig := "" - if k.KubeConfig != nil { - if authMethod != "kubeconfig" { - return fmt.Errorf("can only provide KubeConfig when AuthMethod=kubeconfig") - } - - if _, err := os.Stat(*k.KubeConfig); err != nil { - return fmt.Errorf("error accessing kubeconfig file: %s", err) - } - kubeConfig = *k.KubeConfig - } - - if k.Pod != "" && (k.Image != "" || k.Command != "" || k.Params != "") { - return fmt.Errorf("can only provide Pod when Image, Command, and Params are empty") - } - if k.Image == "" && !k.AllowRuntimeCommand && !k.AllowRuntimePod { - return fmt.Errorf("must specify a container image to run") - } - streamMethod := "logger" - if k.StreamMethod != nil { - streamMethod = *k.StreamMethod - } - if streamMethod != "logger" && streamMethod != "tcp" { - return fmt.Errorf("stream mode must be logger or tcp") - } - - factory := func(w *Workceptor, unitID string, workType string) WorkUnit { - ku := &kubeUnit{ - BaseWorkUnit: BaseWorkUnit{ - status: StatusFileData{ - ExtraData: &kubeExtraData{ - Image: k.Image, - Command: k.Command, - KubeNamespace: namespace, - KubePod: k.Pod, - KubeConfig: kubeConfig, - }, - }, - }, - authMethod: authMethod, - streamMethod: streamMethod, - baseParams: k.Params, - allowRuntimeAuth: k.AllowRuntimeAuth, - allowRuntimeCommand: k.AllowRuntimeCommand, - allowRuntimeParams: k.AllowRuntimeParams, - allowRuntimePod: k.AllowRuntimePod, - deletePodOnRestart: !k.KeepPodOnRestart, - namePrefix: fmt.Sprintf("%s-", strings.ToLower(k.WorkType)), - } - ku.BaseWorkUnit.Init(w, unitID, workType) - - return ku - } - - return wc.RegisterWorker(k.WorkType, factory, false) -} diff --git a/pkg/workceptor/lock_test.go b/pkg/workceptor/lock_test.go index 7c93d6880..a0f0a9312 100644 --- a/pkg/workceptor/lock_test.go +++ b/pkg/workceptor/lock_test.go @@ -1,3 +1,4 @@ +//go:build !no_workceptor // +build !no_workceptor package workceptor @@ -34,7 +35,7 @@ func TestStatusFileLock(t *testing.T) { totalWaitTime += waitTime go func(iter int, waitTime time.Duration) { sfd := StatusFileData{} - err = sfd.UpdateFullStatus(statusFilename, func(status *StatusFileData) { + sfd.UpdateFullStatus(statusFilename, func(status *StatusFileData) { time.Sleep(waitTime) status.State = iter status.StdoutSize = int64(iter) diff --git a/pkg/workceptor/python.go b/pkg/workceptor/python.go index d50d479ca..7fb98f954 100644 --- a/pkg/workceptor/python.go +++ b/pkg/workceptor/python.go @@ -1,3 +1,4 @@ +//go:build !no_workceptor // +build !no_workceptor package workceptor @@ -78,33 +79,3 @@ func init() { cmdline.RegisterConfigTypeForApp("receptor-workers", "work-python", "Run a worker using a Python plugin", workPythonCfg{}, cmdline.Section(workersSection)) } - -// Python executes python code. -type Python struct { - // Name for this worker type. - WorkType string `mapstructure:"work-type"` - // Python module name of the worker plugin. - Plugin string `mapstructure:"plugin"` - // Receptor-exported function to call. - Function string `mapstructure:"function"` - // Plugin-specific configuration. - Config map[string]interface{} `mapstructure:"config"` -} - -func (p Python) setup(wc *Workceptor) error { - factory := func(w *Workceptor, unitID string, workType string) WorkUnit { - cw := &pythonUnit{ - commandUnit: commandUnit{ - BaseWorkUnit: BaseWorkUnit{status: StatusFileData{ExtraData: &commandExtraData{}}}, - }, - plugin: p.Plugin, - function: p.Function, - config: p.Config, - } - cw.BaseWorkUnit.Init(w, unitID, workType) - - return cw - } - - return wc.RegisterWorker(p.WorkType, factory, false) -} diff --git a/pkg/workceptor/remote_work.go b/pkg/workceptor/remote_work.go index 2c1d8013b..8ef54f766 100644 --- a/pkg/workceptor/remote_work.go +++ b/pkg/workceptor/remote_work.go @@ -14,10 +14,10 @@ import ( "path" "regexp" "strings" - "sync" "time" "github.com/ansible/receptor/pkg/logger" + "github.com/ansible/receptor/pkg/netceptor" "github.com/ansible/receptor/pkg/utils" ) @@ -50,7 +50,7 @@ func (rw *remoteUnit) connectToRemote(ctx context.Context) (net.Conn, *bufio.Rea if !ok { return nil, nil, fmt.Errorf("remote ExtraData missing") } - tlsConfig, err := rw.w.nc.GetClientTLSConfig(red.TLSClient, red.RemoteNode, "receptor") + tlsConfig, err := rw.w.nc.GetClientTLSConfig(red.TLSClient, red.RemoteNode, netceptor.ExpectedHostnameTypeReceptor) if err != nil { return nil, nil, err } @@ -62,12 +62,12 @@ func (rw *remoteUnit) connectToRemote(ctx context.Context) (net.Conn, *bufio.Rea ctxChild, _ := context.WithTimeout(ctx, 5*time.Second) hello, err := utils.ReadStringContext(ctxChild, reader, '\n') if err != nil { - conn.Close() + conn.CloseConnection() return nil, nil, err } if !strings.Contains(hello, red.RemoteNode) { - conn.Close() + conn.CloseConnection() return nil, nil, fmt.Errorf("while expecting node ID %s, got message: %s", red.RemoteNode, strings.TrimRight(hello, "\n")) @@ -84,7 +84,7 @@ func (rw *remoteUnit) getConnection(ctx context.Context) (net.Conn, *bufio.Reade if err == nil { return conn, reader } - logger.Warning("Connection to %s failed with error: %s", + logger.Debug("Connection to %s failed with error: %s", rw.Status().ExtraData.(*remoteExtraData).RemoteNode, err) errStr := err.Error() if strings.Contains(errStr, "CRYPTO_ERROR") { @@ -144,16 +144,7 @@ func (rw *remoteUnit) getConnectionAndRun(ctx context.Context, firstTimeSync boo // startRemoteUnit makes a single attempt to start a remote unit. func (rw *remoteUnit) startRemoteUnit(ctx context.Context, conn net.Conn, reader *bufio.Reader) error { - closeOnce := sync.Once{} - doClose := func() error { - var err error - closeOnce.Do(func() { - err = conn.Close() - }) - - return err - } - defer doClose() + defer conn.(interface{ CloseConnection() error }).CloseConnection() red := rw.UnredactedStatus().ExtraData.(*remoteExtraData) workSubmitCmd := make(map[string]interface{}) for k, v := range red.RemoteParams { @@ -184,8 +175,6 @@ func (rw *remoteUnit) startRemoteUnit(ctx context.Context, conn net.Conn, reader } response, err := utils.ReadStringContext(ctx, reader, '\n') if err != nil { - conn.Close() - return fmt.Errorf("read error reading from %s: %s", red.RemoteNode, err) } submitIDRegex := regexp.MustCompile(`with ID ([a-zA-Z0-9]+)\.`) @@ -206,14 +195,12 @@ func (rw *remoteUnit) startRemoteUnit(ctx context.Context, conn net.Conn, reader if err != nil { return fmt.Errorf("error sending stdin file: %s", err) } - err = doClose() + err = conn.Close() if err != nil { return fmt.Errorf("error closing stdin file: %s", err) } response, err = utils.ReadStringContext(ctx, reader, '\n') if err != nil { - conn.Close() - return fmt.Errorf("read error reading from %s: %s", red.RemoteNode, err) } resultErrorRegex := regexp.MustCompile("ERROR: (.*)") @@ -231,8 +218,9 @@ func (rw *remoteUnit) startRemoteUnit(ctx context.Context, conn net.Conn, reader // cancelOrReleaseRemoteUnit makes a single attempt to cancel or release a remote unit. func (rw *remoteUnit) cancelOrReleaseRemoteUnit(ctx context.Context, conn net.Conn, reader *bufio.Reader, - release bool, force bool) error { - defer conn.Close() + release bool, force bool, +) error { + defer conn.(interface{ CloseConnection() error }).CloseConnection() red := rw.Status().ExtraData.(*remoteExtraData) var workCmd string if release { @@ -262,8 +250,6 @@ func (rw *remoteUnit) cancelOrReleaseRemoteUnit(ctx context.Context, conn net.Co } response, err := utils.ReadStringContext(ctx, reader, '\n') if err != nil { - conn.Close() - return fmt.Errorf("read error reading from %s: %s", red.RemoteNode, err) } if response[:5] == "ERROR" { @@ -289,9 +275,15 @@ func (rw *remoteUnit) monitorRemoteStatus(mw *utils.JobContext, forRelease bool) remoteNode := red.RemoteNode remoteUnitID := red.RemoteUnitID conn, reader := rw.getConnection(mw) + defer func() { + if conn != nil { + conn.(interface{ CloseConnection() error }).CloseConnection() + } + }() if conn == nil { return } + writeStatusFailures := 0 for { if conn == nil { conn, reader = rw.getConnection(mw) @@ -302,7 +294,7 @@ func (rw *remoteUnit) monitorRemoteStatus(mw *utils.JobContext, forRelease bool) _, err := conn.Write([]byte(fmt.Sprintf("work status %s\n", remoteUnitID))) if err != nil { logger.Debug("Write error sending to %s: %s\n", remoteUnitID, err) - _ = conn.Close() + _ = conn.(interface{ CloseConnection() error }).CloseConnection() conn = nil continue @@ -310,7 +302,7 @@ func (rw *remoteUnit) monitorRemoteStatus(mw *utils.JobContext, forRelease bool) status, err := utils.ReadStringContext(mw, reader, '\n') if err != nil { logger.Debug("Read error reading from %s: %s\n", remoteNode, err) - _ = conn.Close() + _ = conn.(interface{ CloseConnection() error }).CloseConnection() conn = nil continue @@ -339,6 +331,16 @@ func (rw *remoteUnit) monitorRemoteStatus(mw *utils.JobContext, forRelease bool) return } rw.UpdateBasicStatus(si.State, si.Detail, si.StdoutSize) + if rw.LastUpdateError() != nil { + writeStatusFailures++ + if writeStatusFailures > 3 { + logger.Error("Exceeded retries for updating status file for work unit %s", rw.unitID) + + return + } + } else { + writeStatusFailures = 0 + } if err != nil { logger.Error("Error saving local status file: %s\n", err) @@ -397,6 +399,11 @@ func (rw *remoteUnit) monitorRemoteStdout(mw *utils.JobContext) { return } else if diskStdoutSize < remoteStdoutSize { conn, reader := rw.getConnection(mw) + defer func() { + if conn != nil { + _ = conn.(interface{ CloseConnection() error }).CloseConnection() + } + }() if conn == nil { return } @@ -454,7 +461,7 @@ func (rw *remoteUnit) monitorRemoteStdout(mw *utils.JobContext) { if ok { cr.CancelRead() } - _ = conn.Close() + _ = conn.(interface{ CloseConnection() error }).CloseConnection() return } @@ -462,7 +469,13 @@ func (rw *remoteUnit) monitorRemoteStdout(mw *utils.JobContext) { _, err = io.Copy(stdout, conn) close(doneChan) if err != nil { - logger.Warning("Error copying to stdout file %s: %s\n", rw.stdoutFileName, err) + var errmsg string + if strings.HasSuffix(err.Error(), "error code 499") { + errmsg = "read operation cancelled" + } else { + errmsg = err.Error() + } + logger.Warning("Could not copy to stdout file %s: %s\n", rw.stdoutFileName, errmsg) continue } @@ -649,7 +662,7 @@ func (rw *remoteUnit) cancelOrRelease(release bool, force bool) error { } rw.topJC.NewJob(rw.w.ctx, 1, false) - return rw.runAndMonitor(rw.topJC, true, func(ctx context.Context, conn net.Conn, reader *bufio.Reader) error { + return rw.runAndMonitor(rw.topJC, release, func(ctx context.Context, conn net.Conn, reader *bufio.Reader) error { return rw.cancelOrReleaseRemoteUnit(ctx, conn, reader, release, false) }) } diff --git a/pkg/workceptor/stdio_utils.go b/pkg/workceptor/stdio_utils.go index 248912e0c..8a1452477 100644 --- a/pkg/workceptor/stdio_utils.go +++ b/pkg/workceptor/stdio_utils.go @@ -1,8 +1,10 @@ +//go:build !no_workceptor // +build !no_workceptor package workceptor import ( + "errors" "io" "os" "path" @@ -68,9 +70,19 @@ type stdinReader struct { doneOnce sync.Once } +var errFileSizeZero = errors.New("file is empty") + // newStdinReader allocates a new stdinReader, which reads from a stdin file and provides a Done function. func newStdinReader(unitdir string) (*stdinReader, error) { - reader, err := os.Open(path.Join(unitdir, "stdin")) + stdinpath := path.Join(unitdir, "stdin") + stat, err := os.Stat(stdinpath) + if err != nil { + return nil, err + } + if stat.Size() == 0 { + return nil, errFileSizeZero + } + reader, err := os.Open(stdinpath) if err != nil { return nil, err } diff --git a/pkg/workceptor/workceptor.go b/pkg/workceptor/workceptor.go index c9af66ebc..4cd2649e8 100644 --- a/pkg/workceptor/workceptor.go +++ b/pkg/workceptor/workceptor.go @@ -62,7 +62,7 @@ func New(ctx context.Context, nc *netceptor.Netceptor, dataDir string) (*Workcep signingExpiration: 5 * time.Minute, verifyingKey: "", } - err := w.RegisterWorker("remote", newRemoteWorker, true) + err := w.RegisterWorker("remote", newRemoteWorker, false) if err != nil { return nil, fmt.Errorf("could not register remote worker function: %s", err) } @@ -154,9 +154,9 @@ func (w *Workceptor) createSignature(nodeID string) (string, error) { } exp := time.Now().Add(w.signingExpiration) - claims := &jwt.StandardClaims{ - ExpiresAt: exp.Unix(), - Audience: nodeID, + claims := &jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(exp), + Audience: []string{nodeID}, } rsaPrivateKey, err := certificates.LoadPrivateKey(w.signingKey) if err != nil { @@ -171,7 +171,12 @@ func (w *Workceptor) createSignature(nodeID string) (string, error) { return tokenString, nil } -func (w *Workceptor) ShouldVerifySignature(workType string) bool { +func (w *Workceptor) ShouldVerifySignature(workType string, signWork bool) bool { + // if work unit is remote, just get the signWork boolean from the + // remote extra data field + if workType == "remote" { + return signWork + } w.workTypesLock.RLock() wt, ok := w.workTypes[workType] w.workTypesLock.RUnlock() @@ -193,7 +198,7 @@ func (w *Workceptor) VerifySignature(signature string) error { if err != nil { return fmt.Errorf("could not load verifying key file: %s", err.Error()) } - token, err := jwt.ParseWithClaims(signature, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(signature, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { return rsaPublicKey, nil }) if err != nil { @@ -202,7 +207,7 @@ func (w *Workceptor) VerifySignature(signature string) error { if !token.Valid { return fmt.Errorf("token not valid") } - claims := token.Claims.(*jwt.StandardClaims) + claims := token.Claims.(*jwt.RegisteredClaims) ok := claims.VerifyAudience(w.nc.NodeID(), true) if !ok { return fmt.Errorf("token audience did not match node ID") @@ -241,7 +246,7 @@ func (w *Workceptor) AllocateUnit(workTypeName string, params map[string]string) // AllocateRemoteUnit creates a new remote work unit and generates a local identifier for it. func (w *Workceptor) AllocateRemoteUnit(remoteNode, remoteWorkType, tlsClient, ttl string, signWork bool, params map[string]string) (WorkUnit, error) { if tlsClient != "" { - _, err := w.nc.GetClientTLSConfig(tlsClient, "testhost", "receptor") + _, err := w.nc.GetClientTLSConfig(tlsClient, "testhost", netceptor.ExpectedHostnameTypeReceptor) if err != nil { return nil, err } @@ -301,7 +306,7 @@ func (w *Workceptor) scanForUnit(unitID string) { } ident := fi.Name() w.activeUnitsLock.RLock() - _, ok := w.activeUnits[ident] + _, ok := w.activeUnits[ident] //nolint:ifshort w.activeUnitsLock.RUnlock() if !ok { statusFilename := path.Join(unitdir, "status") @@ -445,29 +450,44 @@ func sleepOrDone(doneChan <-chan struct{}, interval time.Duration) bool { } // GetResults returns a live stream of the results of a unit. -func (w *Workceptor) GetResults(unitID string, startPos int64, doneChan chan struct{}) (chan []byte, error) { +func (w *Workceptor) GetResults(ctx context.Context, unitID string, startPos int64) (chan []byte, error) { unit, err := w.findUnit(unitID) if err != nil { return nil, err } resultChan := make(chan []byte) + closeOnce := sync.Once{} + resultClose := func() { + closeOnce.Do(func() { + close(resultChan) + }) + } + unitdir := path.Join(w.dataDir, unitID) + stdoutFilename := path.Join(unitdir, "stdout") + var stdout *os.File + ctxChild, cancel := context.WithCancel(ctx) go func() { - unitdir := path.Join(w.dataDir, unitID) - stdoutFilename := path.Join(unitdir, "stdout") + defer func() { + err = stdout.Close() + if err != nil { + logger.Error("Error closing stdout %s", stdoutFilename) + } + resultClose() + cancel() + }() + // Wait for stdout file to exist for { - _, err := os.Stat(stdoutFilename) - + stdout, err = os.Open(stdoutFilename) switch { case err == nil: case os.IsNotExist(err): if IsComplete(unit.Status().State) { - close(resultChan) logger.Warning("Unit completed without producing any stdout\n") return } - if sleepOrDone(doneChan, 250*time.Millisecond) { + if sleepOrDone(ctx.Done(), 500*time.Millisecond) { return } @@ -480,57 +500,76 @@ func (w *Workceptor) GetResults(unitID string, startPos int64, doneChan chan str break } - var stdout *os.File - var err error filePos := startPos + statChan := make(chan struct{}, 1) + go func() { + failures := 0 + for { + select { + case <-ctxChild.Done(): + return + case <-time.After(1 * time.Second): + _, err := os.Stat(stdoutFilename) + if os.IsNotExist(err) { + failures++ + if failures > 3 { + logger.Error("Exceeded retries for reading stdout %s", stdoutFilename) + statChan <- struct{}{} + + return + } + } else { + failures = 0 + } + } + } + }() for { - if sleepOrDone(doneChan, 250*time.Millisecond) { + if sleepOrDone(ctx.Done(), 250*time.Millisecond) { return } - if stdout == nil { - stdout, err = os.Open(stdoutFilename) - if err != nil { - continue - } - } - for err == nil { - var newPos int64 - newPos, err = stdout.Seek(filePos, 0) - if err != nil { - logger.Warning("Seek error processing stdout: %s\n", err) - + for { + select { + case <-ctx.Done(): return - } - if newPos != filePos { - logger.Warning("Seek error processing stdout\n") - + case <-statChan: return + default: + var newPos int64 + newPos, err = stdout.Seek(filePos, 0) + if err != nil { + logger.Warning("Seek error processing stdout: %s\n", err) + + return + } + if newPos != filePos { + logger.Warning("Seek error processing stdout\n") + + return + } + var n int + buf := make([]byte, utils.NormalBufferSize) + n, err = stdout.Read(buf) + if n > 0 { + filePos += int64(n) + select { + case <-ctx.Done(): + return + case resultChan <- buf[:n]: + } + } } - var n int - buf := make([]byte, utils.NormalBufferSize) - n, err = stdout.Read(buf) - if n > 0 { - filePos += int64(n) - resultChan <- buf[:n] + if err != nil { + break } } if err == io.EOF { - err = stdout.Close() - if err != nil { - logger.Error("Error closing stdout\n") - - return - } - stdout = nil stdoutSize := stdoutSize(unitdir) if IsComplete(unit.Status().State) && stdoutSize >= unit.Status().StdoutSize { - close(resultChan) - logger.Info("Stdout complete - closing channel for: %s \n", unitID) + logger.Debug("Stdout complete - closing channel for: %s \n", unitID) return } - - continue } else if err != nil { logger.Error("Error reading stdout: %s\n", err) @@ -541,36 +580,3 @@ func (w *Workceptor) GetResults(unitID string, startPos int64, doneChan chan str return resultChan, nil } - -// Workers defines a set of workceptors. -type Workers struct { - // Workers executing a command. - Command []Command `mapstructure:"command"` - // Workers running a python plugin. - Python []Python `mapstructure:"python"` - // Workers interfacing with k8s. - Kubernetes []Kubernetes `mapstructure:"kubernetes"` -} - -// Setup attaches all its workers to a workceptor. -func (s *Workers) Setup(wc *Workceptor) error { - for _, w := range s.Command { - if err := w.setup(wc); err != nil { - return fmt.Errorf("could not setup command worker from workers config: %w", err) - } - } - - for _, w := range s.Python { - if err := w.setup(wc); err != nil { - return fmt.Errorf("could not setup python worker from workers config: %w", err) - } - } - - for _, w := range s.Kubernetes { - if err := w.setup(wc); err != nil { - return fmt.Errorf("could not setup kubernetes worker from workers config: %w", err) - } - } - - return nil -} diff --git a/receptor-python-worker/pyproject.toml b/receptor-python-worker/pyproject.toml new file mode 100644 index 000000000..fed528d4a --- /dev/null +++ b/receptor-python-worker/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/receptor-python-worker/setup.cfg b/receptor-python-worker/setup.cfg index 1c8ffb596..18dd7bcb1 100644 --- a/receptor-python-worker/setup.cfg +++ b/receptor-python-worker/setup.cfg @@ -6,10 +6,14 @@ summary = "The receptor-python-worker command is called by Receptor to supervise home-page = https://github.com/ansible/receptor/tree/devel/receptor-python-worker description-file = README.md description-content-type = text/markdown +version = file: .VERSION -[entry_points] +[options] +packages = find: + +[options.entry_points] console_scripts = - eceptor-python-worker = receptor_python_worker:run + receptor-python-worker = receptor_python_worker:run [files] packages = diff --git a/receptor-python-worker/setup.py b/receptor-python-worker/setup.py deleted file mode 100644 index aa2d8a019..000000000 --- a/receptor-python-worker/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup - -setup( - setup_requires=['pbr'], - pbr=True, -) diff --git a/receptorctl/MANIFEST.in b/receptorctl/MANIFEST.in index 72aa2133c..9ca1f6d90 100644 --- a/receptorctl/MANIFEST.in +++ b/receptorctl/MANIFEST.in @@ -1 +1,6 @@ -include requirements.txt # Needs manual inclusion due to PBR bug +recursive-include receptorctl *.py +include .VERSION +exclude .gitignore +exclude tox.ini +exclude build-requirements.txt +exclude test-requirements.txt diff --git a/receptorctl/build-requirements.txt b/receptorctl/build-requirements.txt index 0383b15e7..0fd4cb7ae 100644 --- a/receptorctl/build-requirements.txt +++ b/receptorctl/build-requirements.txt @@ -1,5 +1,5 @@ +-r requirements.txt pip build wheel -PyYAML pre-commit diff --git a/receptorctl/pyproject.toml b/receptorctl/pyproject.toml new file mode 100644 index 000000000..c64fb138c --- /dev/null +++ b/receptorctl/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.black] +exclude = "(build|.eggs|.tox)" diff --git a/receptorctl/receptorctl/__init__.py b/receptorctl/receptorctl/__init__.py index b1140b1da..7dafdf0bb 100644 --- a/receptorctl/receptorctl/__init__.py +++ b/receptorctl/receptorctl/__init__.py @@ -1,2 +1,4 @@ from .cli import run -from .socket_interface import ReceptorControl \ No newline at end of file +from .socket_interface import ReceptorControl + +__all__ = ["run", "ReceptorControl"] diff --git a/receptorctl/receptorctl/__main__.py b/receptorctl/receptorctl/__main__.py index 4b21d6cf5..27cfa4889 100644 --- a/receptorctl/receptorctl/__main__.py +++ b/receptorctl/receptorctl/__main__.py @@ -1,4 +1,3 @@ -import sys from .cli import run run() diff --git a/receptorctl/receptorctl/cli.py b/receptorctl/receptorctl/cli.py index cc1246d64..01773bd77 100644 --- a/receptorctl/receptorctl/cli.py +++ b/receptorctl/receptorctl/cli.py @@ -7,7 +7,6 @@ import termios import click import json -from pprint import pprint from functools import partial import dateutil.parser import pkg_resources @@ -19,8 +18,8 @@ class IgnoreRequiredWithHelp(click.Group): def parse_args(self, ctx, args): try: return super(IgnoreRequiredWithHelp, self).parse_args(ctx, args) - except click.MissingParameter as exc: - if '--help' not in args: + except click.MissingParameter: + if "--help" not in args: raise # remove the required params so that help can display @@ -29,53 +28,126 @@ def parse_args(self, ctx, args): return super(IgnoreRequiredWithHelp, self).parse_args(ctx, args) +def print_json(json_data): + click.echo(json.dumps(json_data, indent=4, sort_keys=True)) + + +def print_message(message="", nl=True): + click.echo(message, nl=nl) + + +def print_warning(message, nl=True): + click.echo(click.style(f"Warning: {message}", fg="magenta"), err=True, nl=nl) + + +def print_error(message, nl=True): + click.echo(click.style(f"ERROR: {message}", fg="red"), err=True, nl=nl) + + @click.group(cls=IgnoreRequiredWithHelp) @click.pass_context -@click.option('--socket', envvar='RECEPTORCTL_SOCKET', required=True, show_envvar=True, - help="Control socket address to connect to Receptor (defaults to Unix socket, use tcp:// for TCP socket)") -@click.option('--config', '-c', default=None, envvar='RECEPTORCTL_CONFIG', required=False, show_envvar=True, - help="Config filename configured for receptor") -@click.option('--tls-client', 'tlsclient', default=None, envvar='RECEPTORCTL_TLSCLIENT', required=False, show_envvar=True, - help="TLS client name specified in config") -@click.option('--rootcas', default=None, help="Root CA bundle to use instead of system trust when connecting with tls") -@click.option('--key', default=None, help="Client private key filename") -@click.option('--cert', default=None, help="Client certificate filename") -@click.option('--insecureskipverify', default=False, help="Accept any server cert", show_default=True) +@click.option( + "--socket", + envvar="RECEPTORCTL_SOCKET", + required=True, + show_envvar=True, + help="Control socket address to connect to Receptor (defaults to Unix socket, use tcp:// for TCP socket)", # noqa: E501 +) +@click.option( + "--config", + "-c", + default=None, + envvar="RECEPTORCTL_CONFIG", + required=False, + show_envvar=True, + help="Config filename configured for receptor", +) +@click.option( + "--tls-client", + "tlsclient", + default=None, + envvar="RECEPTORCTL_TLSCLIENT", + required=False, + show_envvar=True, + help="TLS client name specified in config", +) +@click.option( + "--rootcas", + default=None, + help="Root CA bundle to use instead of system trust when connecting with tls", +) +@click.option("--key", default=None, help="Client private key filename") +@click.option("--cert", default=None, help="Client certificate filename") +@click.option( + "--insecureskipverify", + default=False, + help="Accept any server cert", + show_default=True, +) def cli(ctx, socket, config, tlsclient, rootcas, key, cert, insecureskipverify): - ctx.obj = dict() - ctx.obj['rc'] = ReceptorControl(socket, config=config, tlsclient=tlsclient, rootcas=rootcas, key=key, cert=cert, insecureskipverify=insecureskipverify) + ctx.obj = { + "rc": None, + "receptorctlVersion": pkg_resources.get_distribution("receptorctl").version, + "receptorVersion": "Unknown", + } + # If we got a socket parameter we can make a ReceptorControl object + if ctx.params.get("socket", None) is not None: + ctx.obj["rc"] = ReceptorControl( + socket, + config=config, + tlsclient=tlsclient, + rootcas=rootcas, + key=key, + cert=cert, + insecureskipverify=insecureskipverify, + ) + # Load and stash the versions + ctx.obj["receptorVersion"] = ctx.obj["rc"].simple_command( + '{"command":"status","requested_fields":["Version"]}' + )["Version"] + # If they mismatch throw a stderr warning + if ctx.obj["receptorVersion"] != ctx.obj["receptorctlVersion"]: + click.echo( + click.style( + "Warning: receptorctl and receptor are different versions, they may not be compatible", # noqa E501 + fg="magenta", + ), + err=True, + ) + + def get_rc(ctx): - return ctx.obj['rc'] + return ctx.obj["rc"] @cli.command(help="Show the status of the Receptor network.") @click.pass_context -@click.option('--json', 'printjson', help="Print as JSON", is_flag=True) +@click.option("--json", "printjson", help="Print as JSON", is_flag=True) def status(ctx, printjson): rc = get_rc(ctx) status = rc.simple_command("status") if printjson: - print(json.dumps(status)) + print_json(status) return - node_id = status.pop('NodeID') - print(f"Node ID: {node_id}") - version = status.pop('Version') - print(f"Version: {version}") - sysCPU = status.pop('SystemCPUCount') - print(f"System CPU Count: {sysCPU}") - sysMemory = status.pop('SystemMemoryMiB') - print(f"System Memory MiB: {sysMemory}") + node_id = status.pop("NodeID") + print_message(f"Node ID: {node_id}") + version = status.pop("Version") + print_message(f"Version: {version}") + sysCPU = status.pop("SystemCPUCount") + print_message(f"System CPU Count: {sysCPU}") + sysMemory = status.pop("SystemMemoryMiB") + print_message(f"System Memory MiB: {sysMemory}") longest_node = 12 - connections = status.pop('Connections', None) + connections = status.pop("Connections", None) if connections: for conn in connections: - l = len(conn['NodeID']) - if l > longest_node: - longest_node = l + length = len(conn["NodeID"]) + if length > longest_node: + longest_node = length - costs = status.pop('KnownConnectionCosts', None) + costs = status.pop("KnownConnectionCosts", None) if costs: for node in costs: if len(node) > longest_node: @@ -83,42 +155,48 @@ def status(ctx, printjson): if connections: for conn in connections: - l = len(conn['NodeID']) - if l > longest_node: - longest_node = l - print() - print(f"{'Connection':<{longest_node}} Cost") + length = len(conn["NodeID"]) + if length > longest_node: + longest_node = length + print_message("") + print_message(f"{'Connection':<{longest_node}} Cost") for conn in connections: - print(f"{conn['NodeID']:<{longest_node}} {conn['Cost']}") + print_message(f"{conn['NodeID']:<{longest_node}} {conn['Cost']}") if costs: - print() - print(f"{'Known Node':<{longest_node}} Known Connections") + print_message() + print_message(f"{'Known Node':<{longest_node}} Known Connections") for node in costs: - print(f"{node:<{longest_node}} ", end="") - pprint(costs[node]) + print_message(f"{node:<{longest_node}} ", nl=False) + for peer, cost in costs[node].items(): + print_message(f"{peer}: {cost} ", nl=False) + print_message() - routes = status.pop('RoutingTable', None) + routes = status.pop("RoutingTable", None) if routes: - print() - print(f"{'Route':<{longest_node}} Via") + print_message() + print_message(f"{'Route':<{longest_node}} Via") for node in routes: - print(f"{node:<{longest_node}} {routes[node]}") + print_message(f"{node:<{longest_node}} {routes[node]}") - ads = status.pop('Advertisements', None) + ads = status.pop("Advertisements", None) if ads: - print() - print(f"{'Node':<{longest_node}} Service Type Last Seen Tags") + print_message() + print_message( + f"{'Node':<{longest_node}} Service Type Last Seen Tags" + ) for ad in ads: - time = dateutil.parser.parse(ad['Time']) - if ad['ConnType'] == 0: - conn_type = 'Datagram' - elif ad['ConnType'] == 1: - conn_type = 'Stream' - elif ad['ConnType'] == 2: - conn_type = 'StreamTLS' + time = dateutil.parser.parse(ad["Time"]) + if ad["ConnType"] == 0: + conn_type = "Datagram" + elif ad["ConnType"] == 1: + conn_type = "Stream" + elif ad["ConnType"] == 2: + conn_type = "StreamTLS" last_seen = f"{time:%Y-%m-%d %H:%M:%S}" - print(f"{ad['NodeID']:<{longest_node}} {ad['Service']:<9} {conn_type:<10} {last_seen:<21} {'-' if (ad['Tags'] is None) else str(ad['Tags']):<16}") + print_message( + f"{ad['NodeID']:<{longest_node}} {ad['Service']:<9} {conn_type:<10} {last_seen:<21} {'-' if (ad['Tags'] is None) else str(ad['Tags']):<16}" # noqa: E501 + ) def print_worktypes(header, isSecure): printOnce = True @@ -134,50 +212,51 @@ def print_worktypes(header, isSecure): workTypes.append(wT) if not workTypes: continue - node = ad['NodeID'] + node = ad["NodeID"] if node in seen_nodes: continue else: seen_nodes.append(node) - workTypes = ', '.join(workTypes) + workTypes = ", ".join(workTypes) if printOnce: - print() - print(f"{'Node':<{longest_node}} {header}") + print_message() + print_message(f"{'Node':<{longest_node}} {header}") printOnce = False - print( - f"{ad['NodeID']:<{longest_node}} ", - end="" - ) - print(workTypes) + print_message(f"{ad['NodeID']:<{longest_node}} ", nl=False) + print_message(workTypes) if ads: print_worktypes("Work Types", False) print_worktypes("Secure Work Types", True) if status: - print("Additional data returned from Receptor:") - pprint(status) + print_message("Additional data returned from Receptor:") + print_json(status) @cli.command(help="Ping a Receptor node.") @click.pass_context -@click.argument('node') -@click.option('--count', default=4, help="Number of pings to send", show_default=True) -@click.option('--delay', default=1.0, help="Time to wait between pings", show_default=True) +@click.argument("node") +@click.option("--count", default=4, help="Number of pings to send", show_default=True) +@click.option( + "--delay", default=1.0, help="Time to wait between pings", show_default=True +) def ping(ctx, node, count, delay): rc = get_rc(ctx) ping_error = False for i in range(count): results = rc.simple_command(f"ping {node}") if "Success" in results and results["Success"]: - print(f"Reply from {results['From']} in {results['TimeStr']}") + print_message(f"Reply from {results['From']} in {results['TimeStr']}") else: ping_error = True if "From" in results and "TimeStr" in results: - print(f"Error {results['Error']} from {results['From']} in {results['TimeStr']}") + print_error( + f"{results['Error']} from {results['From']} in {results['TimeStr']}" + ) else: - print(f"Error: {results['Error']}") - if i < count-1: + print_error(f"{results['Error']}") + if i < count - 1: time.sleep(delay) if ping_error: sys.exit(2) @@ -187,38 +266,49 @@ def ping(ctx, node, count, delay): @click.pass_context def reload(ctx): rc = get_rc(ctx) - results = rc.simple_command(f"reload") + results = rc.simple_command("reload") if "Success" in results and results["Success"]: - print(f"Reload successful") + print_message("Reload successful") else: - print(f"Error: {results['Error']}") - if "ERRORCODE 3" in results['Error']: + print_error(f"{results['Error']}") + if "ERRORCODE 3" in results["Error"]: sys.exit(3) - elif "ERRORCODE 4" in results['Error']: + elif "ERRORCODE 4" in results["Error"]: sys.exit(4) else: - sys.exit(4) + sys.exit(5) + @cli.command(help="Do a traceroute to a Receptor node.") @click.pass_context -@click.argument('node') +@click.argument("node") def traceroute(ctx, node): rc = get_rc(ctx) results = rc.simple_command(f"traceroute {node}") for resno in sorted(results, key=lambda r: int(r)): resval = results[resno] - if 'Error' in resval: - print(f"{resno}: Error {resval['Error']} from {resval['From']} in {resval['TimeStr']}") + if "Error" in resval: + print_error( + f"{resno}: Error {resval['Error']} from {resval['From']} in {resval['TimeStr']}" + ) else: - print(f"{resno}: {resval['From']} in {resval['TimeStr']}") + print_message(f"{resno}: {resval['From']} in {resval['TimeStr']}") @cli.command(help="Connect the local terminal to a Receptor service on a remote node.") @click.pass_context -@click.argument('node') -@click.argument('service') -@click.option('--raw', '-r', default=False, is_flag=True, help="Set terminal to raw mode") -@click.option('--tls-client', 'tlsclient', type=str, default="", help="TLS client config name used when connecting to remote node") +@click.argument("node") +@click.argument("service") +@click.option( + "--raw", "-r", default=False, is_flag=True, help="Set terminal to raw mode" +) +@click.option( + "--tls-client", + "tlsclient", + type=str, + default="", + help="TLS client config name used when connecting to remote node", +) def connect(ctx, node, service, raw, tlsclient): rc = get_rc(ctx) rc.connect_to_service(node, service, tlsclient) @@ -249,32 +339,43 @@ def connect(ctx, node, service, raw, tlsclient): rc._socket.send(data.encode()) finally: termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, stdin_tattrs) - print() + print_message() @cli.group(help="Commands related to unit-of-work processing") def work(): pass + @cli.command(help="Show version information for receptorctl and the receptor node") @click.pass_context def version(ctx): - rc = get_rc(ctx) - receptorVersion = rc.simple_command('{"command":"status","requested_fields":["Version"]}')["Version"] - receptorctlVersion = pkg_resources.get_distribution('receptorctl').version - delim = "" - if receptorVersion != receptorctlVersion: - delim = "\t" - print("Warning: receptorctl and receptor are different versions, they may not be compatible") - print(f"{delim}receptorctl {receptorctlVersion}") - print(f"{delim}receptor {receptorVersion}") + print_message(f"receptorctl {ctx.obj['receptorctlVersion']}") + print_message(f"receptor {ctx.obj['receptorVersion']}") @work.command(name="list", help="List known units of work.") -@click.option('--quiet', '-q', is_flag=True, help="Only list unit IDs with no detail") -@click.option('--node', default=None, type=str, help="Receptor node to list work from. Defaults to the local node.") -@click.option('--unit_id', type=str, required=False, default="", help="Only show detail for a specific unit id") -@click.option('--tls-client', 'tlsclient', type=str, default="", help="TLS client config name used when connecting to remote node") +@click.option("--quiet", "-q", is_flag=True, help="Only list unit IDs with no detail") +@click.option( + "--node", + default=None, + type=str, + help="Receptor node to list work from. Defaults to the local node.", +) +@click.option( + "--unit_id", + type=str, + required=False, + default="", + help="Only show detail for a specific unit id", +) +@click.option( + "--tls-client", + "tlsclient", + type=str, + default="", + help="TLS client config name used when connecting to remote node", +) @click.pass_context def list_units(ctx, unit_id, node, tlsclient, quiet): rc = get_rc(ctx) @@ -286,26 +387,75 @@ def list_units(ctx, unit_id, node, tlsclient, quiet): work = rc.simple_command("work list" + unit_id) if quiet: for k in work.keys(): - print(k) + print_message(k) else: - pprint(work) + print_json(work) @work.command(help="Submit a new unit of work.") @click.pass_context -@click.argument('worktype', type=str, required=True) -@click.option('--node', type=str, help="Receptor node to run the work on. Defaults to the local node.") -@click.option('--payload', '-p', type=str, help="File containing unit of work data. Use - for stdin.") -@click.option('--payload-literal', '-l', type=str, help="Use the command line string as the literal unit of work data.") -@click.option('--no-payload', '-n', is_flag=True, help="Send an empty payload.") -@click.option('--tls-client', 'tlsclient', type=str, default="", help="TLS client used when submitting work to a remote node") -@click.option('--ttl', type=str, default="", help="Time to live until remote work must start, e.g. 1h20m30s or 30m10s") -@click.option('--signwork', help="Digitally sign remote work submissions", is_flag=True) -@click.option('--follow', '-f', help="Remain attached to the job and print its results to stdout", is_flag=True) -@click.option('--rm', help="Release unit after completion", is_flag=True) -@click.option('--param', '-a', help="Additional Receptor parameter (key=value format)", multiple=True) -@click.argument('cmdparams', type=str, required=False, nargs=-1) -def submit(ctx, worktype, node, payload, no_payload, payload_literal, tlsclient, ttl, signwork, follow, rm, param, cmdparams): +@click.argument("worktype", type=str, required=True) +@click.option( + "--node", + type=str, + help="Receptor node to run the work on. Defaults to the local node.", +) +@click.option( + "--payload", + "-p", + type=str, + help="File containing unit of work data. Use - for stdin.", +) +@click.option( + "--payload-literal", + "-l", + type=str, + help="Use the command line string as the literal unit of work data.", +) +@click.option("--no-payload", "-n", is_flag=True, help="Send an empty payload.") +@click.option( + "--tls-client", + "tlsclient", + type=str, + default="", + help="TLS client used when submitting work to a remote node", +) +@click.option( + "--ttl", + type=str, + default="", + help="Time to live until remote work must start, e.g. 1h20m30s or 30m10s", +) +@click.option("--signwork", help="Digitally sign remote work submissions", is_flag=True) +@click.option( + "--follow", + "-f", + help="Remain attached to the job and print its results to stdout", + is_flag=True, +) +@click.option("--rm", help="Release unit after completion", is_flag=True) +@click.option( + "--param", + "-a", + help="Additional Receptor parameter (key=value format)", + multiple=True, +) +@click.argument("cmdparams", type=str, required=False, nargs=-1) +def submit( + ctx, + worktype, + node, + payload, + no_payload, + payload_literal, + tlsclient, + ttl, + signwork, + follow, + rm, + param, + cmdparams, +): pcmds = 0 if payload: pcmds += 1 @@ -314,13 +464,15 @@ def submit(ctx, worktype, node, payload, no_payload, payload_literal, tlsclient, if payload_literal: pcmds += 1 if pcmds < 1: - print("Must provide one of --payload, --no-payload or --payload-literal.") + print_error("Must provide one of --payload, --no-payload or --payload-literal.") sys.exit(1) if pcmds > 1: - print("Cannot provide more than one of --payload, --no-payload and --payload-literal.") + print_error( + "Cannot provide more than one of --payload, --no-payload and --payload-literal." + ) sys.exit(1) if rm and not follow: - print("Warning: using --rm without --follow. Unit results will never be seen.") + print_warning("using --rm without --follow. Unit results will never be seen.") if payload_literal: payload_data = f"{payload_literal}\n".encode() elif no_payload: @@ -329,10 +481,14 @@ def submit(ctx, worktype, node, payload, no_payload, payload_literal, tlsclient, if payload == "-": payload_data = sys.stdin.buffer else: - payload_data = open(payload, 'rb') + try: + payload_data = open(payload, "rb") + except Exception as e: + print_error(f"Failed to load payload file: {e}") + sys.exit(1) unitid = None try: - params = dict(s.split('=', 1) for s in param) + params = dict(s.split("=", 1) for s in param) if cmdparams: allparams = [] if "params" in params: @@ -342,14 +498,25 @@ def submit(ctx, worktype, node, payload, no_payload, payload_literal, tlsclient, if node == "": node = None rc = get_rc(ctx) - work = rc.submit_work(worktype, payload_data, node=node, tlsclient=tlsclient, ttl=ttl, signwork=signwork, params=params) - result = work.pop('result') - unitid = work.pop('unitid') + work = rc.submit_work( + worktype, + payload_data, + node=node, + tlsclient=tlsclient, + ttl=ttl, + signwork=signwork, + params=params, + ) + result = work.pop("result") + unitid = work.pop("unitid") if follow: ctx.invoke(results, unit_id=unitid) else: - print("Result: ", result) - print("Unit ID:", unitid) + print_message(f"Result: {result}") + print_message(f"Unit ID: {unitid}") + except Exception as e: + print_error(e) + sys.exit(101) finally: if rm and unitid: op_on_unit_ids(ctx, "release", [unitid]) @@ -357,19 +524,19 @@ def submit(ctx, worktype, node, payload, no_payload, payload_literal, tlsclient, @work.command(help="Get results for a previously or currently running unit of work.") @click.pass_context -@click.argument('unit_id', type=str, required=True) +@click.argument("unit_id", type=str, required=True) def results(ctx, unit_id): rc = get_rc(ctx) resultsfile = rc.get_work_results(unit_id) - for text in iter(partial(resultsfile.readline, 256), b''): + for text in iter(partial(resultsfile.readline, 256), b""): sys.stdout.buffer.write(text) sys.stdout.buffer.flush() rc = get_rc(ctx) status = rc.simple_command(f"work status {unit_id}") state = status.pop("State", 0) - if state == 3: # Failed + if state == 3: # Failed detail = status.pop("Detail", "Unknown") - sys.stderr.write(f"Remote unit failed: {detail}\n") + print_error(f"Remote unit failed: {detail}\n") sys.exit(1) @@ -378,34 +545,44 @@ def op_on_unit_ids(ctx, op, unit_ids): for unit_id in unit_ids: try: res = list(rc.simple_command(f"work {op} {unit_id}").items())[0] - print(f"({res[1]}, {res[0]})") + print_message(f"({res[1]}, {res[0]})") except Exception as e: - print(f"{unit_id}: ERROR: {e}") + print_error(f"{unit_id}: ERROR: {e}") sys.exit(1) @work.command(help="Cancel (kill) one or more units of work.") -@click.argument('unit_ids', nargs=-1) +@click.argument("unit_ids", nargs=-1) @click.pass_context def cancel(ctx, unit_ids): if len(unit_ids) == 0: - print("No unit IDs supplied: Not doing anything") + print_warning("No unit IDs supplied: Not doing anything") return - print("Cancelled:") + print_message("Cancelled:") op_on_unit_ids(ctx, "cancel", unit_ids) @work.command(help="Release (delete) one or more units of work.") -@click.option('--force', help="Delete locally even if we can't reach the remote node", is_flag=True) -@click.argument('unit_ids', nargs=-1) +@click.option( + "--force", + help="Delete locally even if we can't reach the remote node", + is_flag=True, +) +@click.option("--all", help="Delete all work units", is_flag=True) +@click.argument("unit_ids", nargs=-1) @click.pass_context -def release(ctx, force, unit_ids): - if len(unit_ids) == 0: - print("No unit IDs supplied: Not doing anything") +def release(ctx, force, all, unit_ids): + if len(unit_ids) == 0 and not all: + print_warning("No unit IDs supplied: Not doing anything") return op = "release" if not force else "force-release" - print("Released:") - op_on_unit_ids(ctx, op, unit_ids) + print_message("Released:") + if all: + rc = get_rc(ctx) + work = rc.simple_command("work list") + op_on_unit_ids(ctx, op, work.keys()) + else: + op_on_unit_ids(ctx, op, unit_ids) def run(): @@ -414,6 +591,6 @@ def run(): except click.exceptions.Abort: pass except Exception as e: - print("Error:", e) + print_error(e) sys.exit(1) sys.exit(0) diff --git a/receptorctl/receptorctl/socket_interface.py b/receptorctl/receptorctl/socket_interface.py index aaab10e11..8c62ea4f3 100644 --- a/receptorctl/receptorctl/socket_interface.py +++ b/receptorctl/receptorctl/socket_interface.py @@ -1,4 +1,3 @@ -import sys import os import re import io @@ -7,7 +6,7 @@ import json import ssl import yaml -import pkg_resources + def shutdown_write(sock): if isinstance(sock, ssl.SSLSocket): @@ -15,8 +14,18 @@ def shutdown_write(sock): else: sock.shutdown(socket.SHUT_WR) + class ReceptorControl: - def __init__(self, socketaddress, config=None, tlsclient=None, rootcas=None, key=None, cert=None, insecureskipverify=False): + def __init__( + self, + socketaddress, + config=None, + tlsclient=None, + rootcas=None, + key=None, + cert=None, + insecureskipverify=False, + ): if config and any((rootcas, key, cert)): raise RuntimeError("Cannot specify both config and rootcas, key, cert") if config and not tlsclient: @@ -63,7 +72,9 @@ def readconfig(self, config, tlsclient): self._rootcas = key.get("rootcas", self._rootcas) self._key = key.get("key", self._key) self._cert = key.get("cert", self._cert) - self._insecureskipverify = key.get("insecureskipverify", self._insecureskipverify) + self._insecureskipverify = key.get( + "insecureskipverify", self._insecureskipverify + ) break def simple_command(self, command): @@ -74,7 +85,9 @@ def simple_command(self, command): def connect(self): if self._socket is not None: return - m = re.compile("(tcp|tls):(//)?([a-zA-Z0-9-.:]+):([0-9]+)|(unix:(//)?)?([^:]+)").fullmatch(self._socketaddress) + m = re.compile( + "(tcp|tls):(//)?([a-zA-Z0-9-.:]+):([0-9]+)|(unix:(//)?)?([^:]+)" + ).fullmatch(self._socketaddress) if m: unixsocket = m[7] host = m[3] @@ -86,12 +99,19 @@ def connect(self): raise ValueError(f"Socket path does not exist: {path}") self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._socket.connect(path) - self._sockfile = self._socket.makefile('rwb') + self._sockfile = self._socket.makefile("rwb") self.handshake() return elif host and port: self._socket = None - addrs = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + addrs = socket.getaddrinfo( + host, + port, + socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, + socket.AI_PASSIVE, + ) for addr in addrs: family, type, proto, canonname, sockaddr = addr try: @@ -101,18 +121,25 @@ def connect(self): continue try: if protocol == "tls": - context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=self._rootcas) + context = ssl.create_default_context( + purpose=ssl.Purpose.SERVER_AUTH, + cafile=self._rootcas, + ) if self._key and self._cert: - context.load_cert_chain(certfile=self._cert, keyfile=self._key) + context.load_cert_chain( + certfile=self._cert, keyfile=self._key + ) if self._insecureskipverify: context.check_hostname = False - self._socket = context.wrap_socket(self._socket, server_hostname=host) + self._socket = context.wrap_socket( + self._socket, server_hostname=host + ) self._socket.connect(sockaddr) except OSError: self._socket.close() self._socket = None continue - self._sockfile = self._socket.makefile('rwb') + self._sockfile = self._socket.makefile("rwb") break if self._socket is None: raise ValueError(f"Could not connect to host {host} port {port}") @@ -140,7 +167,16 @@ def connect_to_service(self, node, service, tlsclient): if not str.startswith(text, "Connecting"): raise RuntimeError(text) - def submit_work(self, worktype, payload, node=None, tlsclient=None, ttl=None, signwork=False, params=None): + def submit_work( + self, + worktype, + payload, + node=None, + tlsclient=None, + ttl=None, + signwork=False, + params=None, + ): self.connect() if node is None: node = "localhost" @@ -153,25 +189,25 @@ def submit_work(self, worktype, payload, node=None, tlsclient=None, ttl=None, si } if tlsclient: - commandMap['tlsclient'] = tlsclient + commandMap["tlsclient"] = tlsclient if ttl: - commandMap['ttl'] = ttl + commandMap["ttl"] = ttl if signwork: - commandMap['signwork'] = "true" + commandMap["signwork"] = "true" if params: - for k,v in params.items(): + for k, v in params.items(): if k not in commandMap: - if v[0] == '@' and v[:2] != '@@': + if v[0] == "@" and v[:2] != "@@": fname = v[1:] if not os.path.exists(fname): raise FileNotFoundError("{} does not exist".format(fname)) try: - with open(fname, 'r') as f: + with open(fname, "r") as f: v_contents = f.read() - except: + except Exception: raise OSError("could not read from file {}".format(fname)) commandMap[k] = v_contents else: @@ -182,7 +218,9 @@ def submit_work(self, worktype, payload, node=None, tlsclient=None, ttl=None, si command = f"{commandJson}\n" self.writestr(command) text = self.readstr() - m = re.compile("Work unit created with ID (.+). Send stdin data and EOF.").fullmatch(text) + m = re.compile( + "Work unit created with ID (.+). Send stdin data and EOF." + ).fullmatch(text) if not m: errmsg = "Failed to start work unit" if str.startswith(text, "ERROR: "): diff --git a/receptorctl/requirements.txt b/receptorctl/requirements.txt deleted file mode 100644 index 27e24fdf2..000000000 --- a/receptorctl/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -python-dateutil -click -pyyaml diff --git a/receptorctl/setup.cfg b/receptorctl/setup.cfg index 2015dc7f9..f89e3d591 100644 --- a/receptorctl/setup.cfg +++ b/receptorctl/setup.cfg @@ -6,11 +6,23 @@ summary = "Receptorctl is a front-end CLI and importable Python library that int home_page = https://receptor.readthedocs.io description_file = README.md description_content_type = text/markdown +license = "Apache License 2.0" +version = file: .VERSION -[entry_points] +[options] +packages = find: +install_requires = + python-dateutil + click + pyyaml + +[options.entry_points] console_scripts = receptorctl = receptorctl:run [files] packages = receptorctl + +[flake8] +max-line-length = 100 diff --git a/receptorctl/setup.py b/receptorctl/setup.py deleted file mode 100644 index aa2d8a019..000000000 --- a/receptorctl/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup - -setup( - setup_requires=['pbr'], - pbr=True, -) diff --git a/receptorctl/test-requirements.txt b/receptorctl/test-requirements.txt index be53becf7..aa3ad12ae 100644 --- a/receptorctl/test-requirements.txt +++ b/receptorctl/test-requirements.txt @@ -1 +1,7 @@ -pytest==6.1.1 +-r build-requirements.txt +pytest==7.0.1 +urllib3 +black +tox +pylama +pytest-github diff --git a/receptorctl/tests/README.md b/receptorctl/tests/README.md index 9576564e0..89b4d4455 100644 --- a/receptorctl/tests/README.md +++ b/receptorctl/tests/README.md @@ -1,7 +1,15 @@ -# Receptorctl Test Docs +# Testing `receptorctl` -The receptorctl test docs are fairly limited and aim to specifically test the -receptorctl interface rather than test receptor itself. These tests are limited -but appropriate to test the python client for receptor for any projects that -may use it. I think the tests are fairly self explanatory so I will not provide -any detail on how they work. +Tests are run with [`tox`](https://tox.wiki/en/latest/). + +To run tests: + +``` +$ tox -e py3 +``` + +To run the linters: + +``` +$ tox -e lint +``` diff --git a/receptorctl/tests/conftest.py b/receptorctl/tests/conftest.py index a04087459..6e8236d78 100644 --- a/receptorctl/tests/conftest.py +++ b/receptorctl/tests/conftest.py @@ -1,7 +1,3 @@ -import sys - -sys.path.append("../receptorctl") - import receptorctl import pytest @@ -10,143 +6,108 @@ import shutil import time import json +import yaml from click.testing import CliRunner -tmpDir = "/tmp/receptorctltest" +from lib import create_certificate @pytest.fixture(scope="session") -def create_empty_dir(): - def check_dependencies(): - """Check if we have the required dependencies - raise an exception if we don't - """ +def base_tmp_dir(): + receptor_tmp_dir = "/tmp/receptor" + base_tmp_dir = "/tmp/receptorctltest" - # Check if openssl binary is on the path - try: - subprocess.check_output(["openssl", "version"]) - except: - raise Exception( - "openssl binary not found\n" 'Consider run "sudo dnf install openssl"' - ) + # Clean up tmp directory and create a new one + if os.path.exists(base_tmp_dir): + shutil.rmtree(base_tmp_dir) + os.mkdir(base_tmp_dir) - check_dependencies() + yield base_tmp_dir - # Clean up tmp directory and create a new one - if os.path.exists(tmpDir): - shutil.rmtree(tmpDir) - os.mkdir(tmpDir) + # Tear-down + # if os.path.exists(base_tmp_dir): + # shutil.rmtree(base_tmp_dir) + if os.path.exists(receptor_tmp_dir): + shutil.rmtree(receptor_tmp_dir) -@pytest.fixture(scope="session") -def create_certificate(create_empty_dir): - def generate_cert(name, commonName): - keyPath = os.path.join(tmpDir, name + ".key") - crtPath = os.path.join(tmpDir, name + ".crt") - subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) - subprocess.check_output( - [ - "openssl", - "req", - "-x509", - "-new", - "-nodes", - "-key", - keyPath, - "-subj", - "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=ca", - "-sha256", - "-out", - crtPath, - ] - ) - return keyPath, crtPath - - def generate_cert_with_ca(name, caKeyPath, caCrtPath, commonName): - keyPath = os.path.join(tmpDir, name + ".key") - crtPath = os.path.join(tmpDir, name + ".crt") - csrPath = os.path.join(tmpDir, name + ".csa") - extPath = os.path.join(tmpDir, name + ".ext") - - # create x509 extension - with open(extPath, "w") as ext: - ext.write("subjectAltName=DNS:" + commonName) - ext.close() - subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) - - # create cert request - subprocess.check_output( - [ - "openssl", - "req", - "-new", - "-sha256", - "-key", - keyPath, - "-subj", - "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=" + commonName, - "-out", - csrPath, - ] - ) + subprocess.call(["killall", "receptor"]) - # sign cert request - subprocess.check_output( - [ - "openssl", - "x509", - "-req", - "-extfile", - extPath, - "-in", - csrPath, - "-CA", - caCrtPath, - "-CAkey", - caKeyPath, - "-CAcreateserial", - "-out", - crtPath, - "-sha256", - ] - ) - return keyPath, crtPath +@pytest.fixture(scope="class") +def receptor_mesh(base_tmp_dir): + class ReceptorMeshSetup: + # Relative dir to the receptorctl tests + mesh_definitions_dir = "tests/mesh-definitions" + + def __init__(self): + # Default vars + self.base_tmp_dir = base_tmp_dir + + # Required dependencies + self.__check_dependencies() + + def setup(self, mesh_name: str = "mesh1", socket_file_name: str = "node1.sock"): + self.mesh_name = mesh_name + self.__change_config_files_dir(mesh_name) + self.__create_tmp_dir() + self.__create_certificates() + self.socket_file_name = socket_file_name + + # HACK this should be a dinamic way to select a node socket + self.default_socket_unix = "unix://" + os.path.join( + self.get_mesh_tmp_dir(), socket_file_name + ) - # Create a new CA - caKeyPath, caCrtPath = generate_cert("ca", "ca") - clientKeyPath, clientCrtPath = generate_cert_with_ca( - "client", caKeyPath, caCrtPath, "localhost" - ) - generate_cert_with_ca("server", caKeyPath, caCrtPath, "localhost") + def default_receptor_controller_unix(self): + return receptorctl.ReceptorControl(self.default_socket_unix) - return { - "caKeyPath": caKeyPath, - "caCrtPath": caCrtPath, - "clientKeyPath": clientKeyPath, - "clientCrtPath": clientCrtPath, - } + def __change_config_files_dir(self, mesh_name: str): + self.config_files_dir = "{}/{}".format(self.mesh_definitions_dir, mesh_name) + self.config_files = [] + # Iterate over all the files in the config_files_dir + # and create a list of all files that end with .yaml or .yml + for f in os.listdir(self.config_files_dir): + if f.endswith(".yaml") or f.endswith(".yml"): + self.config_files.append(os.path.join(self.config_files_dir, f)) -@pytest.fixture(scope="session") -def certificate_files(create_certificate): - """Returns a dict with the certificate files - - The dict contains the following keys: - caKeyPath - caCrtPath - clientKeyPath - clientCrtPath - """ - return create_certificate + def __create_certificates(self): + self.certificate_files = create_certificate(self.get_mesh_tmp_dir()) + def get_mesh_name(self): + return self.config_files_dir.split("/")[-1] -@pytest.fixture(scope="session") -def prepare_environment(certificate_files): - pass + def get_mesh_tmp_dir(self): + mesh_tmp_dir = "{}/{}".format(self.base_tmp_dir, self.mesh_name) + return mesh_tmp_dir + def __check_dependencies(self): + """Check if we have the required dependencies + raise an exception if we don't + """ -@pytest.fixture(scope="session") + # Check if openssl binary is on the path + try: + subprocess.check_output(["openssl", "version"]) + except FileNotFoundError: + raise Exception( + "openssl binary not found\n" + 'Consider run "sudo dnf install openssl"' + ) + + def __create_tmp_dir(self): + mesh_tmp_dir_path = self.get_mesh_tmp_dir() + + # Clean up tmp directory and create a new one + if os.path.exists(mesh_tmp_dir_path): + shutil.rmtree(mesh_tmp_dir_path) + os.mkdir(mesh_tmp_dir_path) + + return ReceptorMeshSetup() + + +@pytest.fixture(scope="class") def receptor_bin_path(): """Returns the path to the receptor binary @@ -165,7 +126,17 @@ def receptor_bin_path(): # the path to the binary if it is found. receptor_bin_path_from_test_dir = os.path.join( os.path.dirname(os.path.abspath(__file__)), - "../../tests/artifacts-output", + "../../tests/artifacts-output/", + "receptor", + ) + if os.path.exists(receptor_bin_path_from_test_dir): + return receptor_bin_path_from_test_dir + + # Check if the receptor binary is in '../../' and returns + # the path to the binary if it is found. + receptor_bin_path_from_test_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../", "receptor", ) if os.path.exists(receptor_bin_path_from_test_dir): @@ -182,18 +153,18 @@ def receptor_bin_path(): @pytest.fixture(scope="class") -def default_socket_unix(): - return "unix://" + os.path.join(tmpDir, "node1.sock") +def default_socket_tcp(): + return "tcp://localhost:11112" @pytest.fixture(scope="class") -def default_receptor_controller_unix(default_socket_unix): - return receptorctl.ReceptorControl(default_socket_unix) +def default_socket_file(receptor_mesh): + return receptor_mesh.get_mesh_tmp_dir() + "/node1.sock" @pytest.fixture(scope="class") -def default_socket_tcp(): - return "tcp://localhost:11112" +def default_receptor_controller_socket_file(default_socket_file): + return receptorctl.ReceptorControl(default_socket_file) @pytest.fixture(scope="class") @@ -203,7 +174,6 @@ def default_receptor_controller_tcp(default_socket_tcp): @pytest.fixture(scope="class") def default_receptor_controller_tcp_tls(default_socket_tcp, certificate_files): - socketaddress = default_socket_tcp rootcas = certificate_files["caCrtPath"] key = certificate_files["clientKeyPath"] cert = certificate_files["clientCrtPath"] @@ -221,46 +191,149 @@ def default_receptor_controller_tcp_tls(default_socket_tcp, certificate_files): @pytest.fixture(scope="class") -def receptor_mesh( - prepare_environment, receptor_bin_path, default_receptor_controller_unix -): +def receptor_nodes(): + class ReceptorNodes: + nodes = [] + log_files = [] + + return ReceptorNodes() + + +def receptor_nodes_kill(nodes): + for node in nodes: + node.kill() + + for node in nodes: + node.wait(3) + + +def import_config_from_node(node): + """Receive a node and return the config file as a dict""" + stream = open(node.args[2], "r") + try: + config_unflatten = yaml.safe_load(stream) + except yaml.YAMLError as e: + raise e + stream.close() + + config = {} + for c in config_unflatten: + config.update(c) + + return config - node1 = subprocess.Popen( - [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node1.yaml"] - ) - node2 = subprocess.Popen( - [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node2.yaml"] - ) - node3 = subprocess.Popen( - [receptor_bin_path, "-c", "tests/mesh-definitions/mesh1/node3.yaml"] - ) +def receptor_mesh_wait_until_ready(nodes, receptor_controller): time.sleep(0.5) - node1_controller = default_receptor_controller_unix + # Try up to 6 times + tries = 0 while True: - status = node1_controller.simple_command("status") - if status["RoutingTable"] == {"node2": "node2", "node3": "node2"}: + status = receptor_controller.simple_command("status") + # Check if it has three known nodes + if len(status["KnownConnectionCosts"]) == 3: break - time.sleep(0.5) + tries += 1 + if tries > 6: + raise Exception("Receptor Mesh did not start up") + time.sleep(1) + + receptor_controller.close() + + +@pytest.fixture(scope="class") +def certificate_files(receptor_mesh): + return receptor_mesh.certificate_files + + +@pytest.fixture(scope="class") +def default_receptor_controller_unix(receptor_mesh): + return receptor_mesh.default_receptor_controller_unix() + + +def start_nodes(receptor_mesh, receptor_nodes, receptor_bin_path): + for i, config_file in enumerate(receptor_mesh.config_files): + log_file_name = ( + config_file.split("/")[-1].replace(".yaml", ".log").replace(".yml", ".log") + ) + receptor_nodes.log_files.append( + open( + os.path.join(receptor_mesh.get_mesh_tmp_dir(), log_file_name), + "w", + ) + ) + receptor_nodes.nodes.append( + subprocess.Popen( + [receptor_bin_path, "-c", config_file], + stdout=receptor_nodes.log_files[i], + stderr=receptor_nodes.log_files[i], + ) + ) + + +@pytest.fixture(scope="class") +def receptor_mesh_mesh1( + receptor_bin_path, + receptor_nodes, + receptor_mesh, +): + # Set custom config files dir + receptor_mesh.setup("mesh1") + + # Start the receptor nodes processes + start_nodes(receptor_mesh, receptor_nodes, receptor_bin_path) + + receptor_mesh_wait_until_ready( + receptor_nodes.nodes, receptor_mesh.default_receptor_controller_unix() + ) + + yield + + receptor_nodes_kill(receptor_nodes.nodes) + - node1_controller.close() +@pytest.fixture(scope="class") +def receptor_mesh_access_control( + receptor_bin_path, + receptor_nodes, + receptor_mesh, +): + # Set custom config files dir + receptor_mesh.setup("access_control", "node2.sock") + + # Create PEM key for signed work + key_path = os.path.join(receptor_mesh.get_mesh_tmp_dir(), "signwork_key") + subprocess.check_output( + [ + "ssh-keygen", + "-b", + "2048", + "-t", + "rsa", + "-f", + key_path, + "-q", + "-N", + "", + ] + ) - # Debug mesh data - print("# Mesh nodes: {}".format(str(status["KnownConnectionCosts"].keys()))) + # Start the receptor nodes processes + start_nodes(receptor_mesh, receptor_nodes, receptor_bin_path) + + receptor_mesh_wait_until_ready( + receptor_nodes.nodes, receptor_mesh.default_receptor_controller_unix() + ) yield - node1.kill() - node2.kill() - node1.wait() - node2.wait() + receptor_nodes_kill(receptor_nodes.nodes) @pytest.fixture(scope="function") -def receptor_control_args(): +def receptor_control_args(receptor_mesh): args = { - "--socket": "/tmp/receptorctltest/node1.sock", + "--socket": f"{receptor_mesh.get_mesh_tmp_dir()}/{receptor_mesh.socket_file_name}", "--config": None, "--tls": None, "--rootcas": None, @@ -293,7 +366,8 @@ def parse_args_to_list(args: dict): arg_list.append(str(v)) return arg_list - runner = CliRunner() + # Since we may log errors/warnings on stderr we want to split stdout and stderr + runner = CliRunner(mix_stderr=False) out = runner.invoke( receptorctl.cli.cli, @@ -318,7 +392,8 @@ def f_invoke_as_json(command, args: list = []): """ result = invoke(command, ["--json"] + args) try: - json_output = json.loads(result.output) + # JSON data should only be on stdout + json_output = json.loads(result.stdout) except json.decoder.JSONDecodeError: pytest.fail("The command is not in json format") return result, json_output diff --git a/receptorctl/tests/lib.py b/receptorctl/tests/lib.py new file mode 100644 index 000000000..7398e9042 --- /dev/null +++ b/receptorctl/tests/lib.py @@ -0,0 +1,95 @@ +import os +import subprocess + + +def __init__(): + pass + + +def create_certificate(tmp_dir: str): + def generate_cert(name, commonName): + keyPath = os.path.join(tmp_dir, name + ".key") + crtPath = os.path.join(tmp_dir, name + ".crt") + subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) + subprocess.check_output( + [ + "openssl", + "req", + "-x509", + "-new", + "-nodes", + "-key", + keyPath, + "-subj", + "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=ca", + "-sha256", + "-out", + crtPath, + ] + ) + return keyPath, crtPath + + def generate_cert_with_ca(name, caKeyPath, caCrtPath, commonName): + keyPath = os.path.join(tmp_dir, name + ".key") + crtPath = os.path.join(tmp_dir, name + ".crt") + csrPath = os.path.join(tmp_dir, name + ".csa") + extPath = os.path.join(tmp_dir, name + ".ext") + + # create x509 extension + with open(extPath, "w") as ext: + ext.write("subjectAltName=DNS:" + commonName) + ext.close() + subprocess.check_output(["openssl", "genrsa", "-out", keyPath, "2048"]) + + # create cert request + subprocess.check_output( + [ + "openssl", + "req", + "-new", + "-sha256", + "-key", + keyPath, + "-subj", + "/C=/ST=/L=/O=/OU=ReceptorTesting/CN=" + commonName, + "-out", + csrPath, + ] + ) + + # sign cert request + subprocess.check_output( + [ + "openssl", + "x509", + "-req", + "-extfile", + extPath, + "-in", + csrPath, + "-CA", + caCrtPath, + "-CAkey", + caKeyPath, + "-CAcreateserial", + "-out", + crtPath, + "-sha256", + ] + ) + + return keyPath, crtPath + + # Create a new CA + caKeyPath, caCrtPath = generate_cert("ca", "ca") + clientKeyPath, clientCrtPath = generate_cert_with_ca( + "client", caKeyPath, caCrtPath, "localhost" + ) + generate_cert_with_ca("server", caKeyPath, caCrtPath, "localhost") + + return { + "caKeyPath": caKeyPath, + "caCrtPath": caCrtPath, + "clientKeyPath": clientKeyPath, + "clientCrtPath": clientCrtPath, + } diff --git a/receptorctl/tests/mesh-definitions/access_control/node1.yaml b/receptorctl/tests/mesh-definitions/access_control/node1.yaml new file mode 100644 index 000000000..daa7d98f8 --- /dev/null +++ b/receptorctl/tests/mesh-definitions/access_control/node1.yaml @@ -0,0 +1,28 @@ +- node: + id: node1 + +- log-level: debug + +- tcp-listener: + port: 12111 + +- control-service: + filename: /tmp/receptorctltest/access_control/node1.sock + +- work-signing: + privatekey: /tmp/receptorctltest/access_control/signwork_key + tokenexpiration: 10h30m + +- work-verification: + publickey: /tmp/receptorctltest/access_control/signwork_key.pub + +- work-command: + worktype: signed-echo + command: bash + params: "-c \"for w in {1..4}; do echo ${line^^}; sleep 1; done\"" + verifysignature: true + +- work-command: + workType: unsigned-echo + command: bash + params: "-c \"for w in {1..4}; do echo ${line^^}; sleep 1; done\"" diff --git a/receptorctl/tests/mesh-definitions/access_control/node2.yaml b/receptorctl/tests/mesh-definitions/access_control/node2.yaml new file mode 100644 index 000000000..fbe87d529 --- /dev/null +++ b/receptorctl/tests/mesh-definitions/access_control/node2.yaml @@ -0,0 +1,13 @@ +- node: + id: node2 + +- log-level: debug + +- tcp-peer: + address: localhost:12111 + +- tcp-listener: + port: 12121 + +- control-service: + filename: /tmp/receptorctltest/access_control/node2.sock diff --git a/receptorctl/tests/mesh-definitions/access_control/node3.yaml b/receptorctl/tests/mesh-definitions/access_control/node3.yaml new file mode 100644 index 000000000..58eee859a --- /dev/null +++ b/receptorctl/tests/mesh-definitions/access_control/node3.yaml @@ -0,0 +1,10 @@ +- node: + id: node3 + +- log-level: debug + +- tcp-peer: + address: localhost:12121 + +- control-service: + filename: /tmp/receptorctltest/access_control/node3.sock diff --git a/receptorctl/tests/mesh-definitions/mesh1/node1.yaml b/receptorctl/tests/mesh-definitions/mesh1/node1.yaml index 4fd4621e5..73d63a54d 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node1.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node1.yaml @@ -5,7 +5,7 @@ port: 11111 - control-service: - filename: /tmp/receptorctltest/node1.sock + filename: /tmp/receptorctltest/mesh1/node1.sock - tcp-server: port: 11112 @@ -14,10 +14,10 @@ - tls-server: name: tlsserver - key: /tmp/receptorctltest/server.key - cert: /tmp/receptorctltest/server.crt + key: /tmp/receptorctltest/mesh1/server.key + cert: /tmp/receptorctltest/mesh1/server.crt requireclientcert: true - clientcas: /tmp/receptorctltest/ca.crt + clientcas: /tmp/receptorctltest/mesh1/ca.crt - control-service: service: ctltls diff --git a/receptorctl/tests/mesh-definitions/mesh1/node2.yaml b/receptorctl/tests/mesh-definitions/mesh1/node2.yaml index 52cb4dff8..f1173d792 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node2.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node2.yaml @@ -8,4 +8,4 @@ port: 11121 - control-service: - filename: /tmp/receptorctltest/node2.sock + filename: /tmp/receptorctltest/mesh1/node2.sock diff --git a/receptorctl/tests/mesh-definitions/mesh1/node3.yaml b/receptorctl/tests/mesh-definitions/mesh1/node3.yaml index 1af76c119..ebcadb11a 100644 --- a/receptorctl/tests/mesh-definitions/mesh1/node3.yaml +++ b/receptorctl/tests/mesh-definitions/mesh1/node3.yaml @@ -5,4 +5,15 @@ address: localhost:11121 - control-service: - filename: /tmp/receptorctltest/node3.sock + filename: /tmp/receptorctltest/mesh1/node3.sock + +- work-command: + worktype: sleep + command: bash + params: "-c \"read N_ITER; for i in `seq 1 $N_ITER`; do echo $((${N_ITER}-${i}+1)) 'remaining'; sleep 1; done\"" + allowruntimeparams: true + +- work-command: + workType: echo-uppercase + command: bash + params: "-c \"read PAYLOAD; echo ${PAYLOAD^^}\"" diff --git a/receptorctl/tests/test_cli.py b/receptorctl/tests/test_cli.py index 092f876f9..93e35529f 100644 --- a/receptorctl/tests/test_cli.py +++ b/receptorctl/tests/test_cli.py @@ -1,9 +1,4 @@ -import sys - -sys.path.append("../receptorctl") - from receptorctl import cli as commands -import receptorctl # The goal is to write tests following the click documentation: # https://click.palletsprojects.com/en/8.0.x/testing/ @@ -11,9 +6,9 @@ import pytest -@pytest.mark.usefixtures("receptor_mesh") -class TestCommands: - def test_cmd_status(self, invoke_as_json): +@pytest.mark.usefixtures("receptor_mesh_mesh1") +class TestCLI: + def test_cli_cmd_status(self, invoke_as_json): result, json_output = invoke_as_json(commands.status, []) assert result.exit_code == 0 assert set( diff --git a/receptorctl/tests/test_connection.py b/receptorctl/tests/test_connection.py index e68b602ed..a07c8a858 100644 --- a/receptorctl/tests/test_connection.py +++ b/receptorctl/tests/test_connection.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.usefixtures("receptor_mesh") +@pytest.mark.usefixtures("receptor_mesh_mesh1") class TestReceptorCtlConnection: def test_connect_to_service(self, default_receptor_controller_unix): node1_controller = default_receptor_controller_unix diff --git a/receptorctl/tests/test_mesh.py b/receptorctl/tests/test_mesh.py new file mode 100644 index 000000000..e58f005c8 --- /dev/null +++ b/receptorctl/tests/test_mesh.py @@ -0,0 +1,61 @@ +from receptorctl import cli as commands + +# The goal is to write tests following the click documentation: +# https://click.palletsprojects.com/en/8.0.x/testing/ + +import pytest +import time + + +@pytest.mark.usefixtures("receptor_mesh_access_control") +class TestMeshFirewall: + def test_work_unsigned(self, invoke, receptor_nodes): + """Run a unsigned work-command + + Steps: + 1. Create node1 with a unsigned work-command + 2. Create node2 + 3. Run from node2 a unsigned work-command to node1 + 4. Expect to be accepted + """ + + # Run an unsigned command + result = invoke( + commands.work, + "submit unsigned-echo --node node1 --no-payload".split(), + ) + work_unit_id = result.stdout.split("Unit ID: ")[-1].replace("\n", "") + + time.sleep(5) + assert result.exit_code == 0 + + # Release unsigned work + result = invoke(commands.work, f"release {work_unit_id}".split()) + + assert result.exit_code == 0 + + # DISABLE UNTIL THE FIX BEING IMPLEMENTED + # + # def test_work_signed_expect_block(self, invoke, receptor_nodes): + # """Run a signed work-command without the right key + # and expect to be blocked. + + # Steps: + # 1. Create node1 with a signed work-command + # 2. Create node2 + # 3. Run from node2 a signed work-command to node1 + # 4. Expect to be blocked + # """ + # # Run an unsigned command + # result = invoke( + # commands.work, "submit signed-echo --node node1 --no-payload".split() + # ) + # work_unit_id = result.stdout.split("Unit ID: ")[-1].replace("\n", "") + + # time.sleep(5) + # assert work_unit_id, "Work unit ID should not be empty" + # assert result.exit_code != 0, "Work signed run should fail, but it worked" + + # # Release unsigned work + # result = invoke(commands.work, f"release {work_unit_id}".split()) + # assert result.exit_code == 0, "Work release failed" diff --git a/receptorctl/tests/test_workunit.py b/receptorctl/tests/test_workunit.py new file mode 100644 index 000000000..e92f10ac8 --- /dev/null +++ b/receptorctl/tests/test_workunit.py @@ -0,0 +1,166 @@ +# The goal is to write tests following the click documentation: +# https://click.palletsprojects.com/en/8.0.x/testing/ + +import pytest +import time + + +@pytest.fixture(scope="function") +def wait_for_workunit_state(): + def _wait_for_workunit_state( + node_controller, + unitid: str, + expected_detail: str = None, + expected_state_name: str = None, + timeout_seconds: int = 30, + ) -> bool: + """Wait for a workunit to finish + + At least 'expected_detail' or 'expected_state_name' must be specified. + + Args: + node_controller: The node controller used to create the workunit + unitid: The unitid of the workunit to wait for + expected_detail: The expected detail of the workunit + expected_state_name: The expected state name of the workunit + timeout_seconds: The number of seconds to wait before timing out + + Returns: + True if the workunit finished, False if it timed out + """ + if expected_detail is None and expected_state_name is None: + raise ValueError( + "At least 'expected_detail' or 'expected_state_name' must be specified" + ) + + remaining_time = timeout_seconds + + if expected_detail is not None: + for _ in range(remaining_time): + status = node_controller.simple_command("work status {}".format(unitid)) + if status["Detail"] == expected_detail: + return True + else: + time.sleep(1) + remaining_time -= 1 + + if remaining_time <= 0: + return False + + if expected_state_name is not None: + for _ in range(remaining_time): + status = node_controller.simple_command("work status {}".format(unitid)) + if status["StateName"] == expected_state_name: + return True + else: + time.sleep(1) + remaining_time -= 1 + + return False + + return _wait_for_workunit_state + + +@pytest.fixture(scope="function") +def wait_for_work_finished(wait_for_workunit_state): + def _wait_for_work_finished( + node_controller, unitid: str, timeout_seconds: int = 30 + ) -> bool: + """Wait for a workunit to finish + + Args: + node_controller: The node controller used to create the workunit + unitid: The unitid of the workunit to wait for + timeout_seconds: The number of seconds to wait before timing out + + Returns: + True if the workunit finished, False if it timed out + """ + + return wait_for_workunit_state( + node_controller, + unitid, + expected_detail="exit status 0", + expected_state_name="Succeeded", + timeout_seconds=timeout_seconds, + ) + + return _wait_for_work_finished + + +@pytest.mark.usefixtures("receptor_mesh_mesh1") +class TestWorkUnit: + def test_workunit_simple( + self, + invoke_as_json, + default_receptor_controller_socket_file, + wait_for_work_finished, + ): + # Spawn a long running command + node1_controller = default_receptor_controller_socket_file + + wait_for = 5 # in seconds + + payload = "That's a long string example! And there's emoji too! 👾" + work = node1_controller.submit_work("echo-uppercase", payload, node="node3") + state_result = work.pop("result") + state_unitid = work.pop("unitid") + + assert state_result == "Job Started" + assert wait_for_work_finished( + node1_controller, state_unitid, wait_for + ), "Workunit timed out and never finished" + + work_result = ( + node1_controller.get_work_results(state_unitid) + .read() + .decode("utf-8") + .strip() + ) + + assert payload.upper() == work_result, ( + f"Workunit did not report the expected result:\n - payload: {payload}" + f"\n - work_result: {work_result}" + ) + + node1_controller.close() + + def test_workunit_cmd_cancel( + self, + invoke_as_json, + default_receptor_controller_socket_file, + wait_for_workunit_state, + ): + # Spawn a long running command + node1_controller = default_receptor_controller_socket_file + + sleep_for = 9999 # in seconds + wait_for = 15 # in seconds + + work = node1_controller.submit_work("sleep", str(sleep_for), node="node3") + state_result = work.pop("result") + state_unitid = work.pop("unitid") + assert state_result == "Job Started" + + # HACK: Wait for the workunit to start + # receptor should be able to cancel the workunit with this + time.sleep(5) + + # Run and check cancel command + cancel_output = node1_controller.simple_command(f"work cancel {state_unitid}") + assert cancel_output["cancelled"] == state_unitid + + # Wait workunit detail == 'Cancelled' + assert wait_for_workunit_state( + node1_controller, + state_unitid, + expected_detail="Killed", + expected_state_name="Failed", + timeout_seconds=wait_for, + ), "Workunit timed out and never finished" + + # Get work list and check for the workunit detail state + work_list = node1_controller.simple_command("work list") + assert work_list[state_unitid]["Detail"] == "Killed" + + node1_controller.close() diff --git a/receptorctl/tox.ini b/receptorctl/tox.ini new file mode 100644 index 000000000..f31d92c3d --- /dev/null +++ b/receptorctl/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = py3 +isolated_build = True + +[testenv] +usedevelop = True +deps = + pytest +commands = + py.test -v tests {posargs} + +[testenv:lint] +deps = + black + flake8 +commands = + black --check . + flake8 diff --git a/tests/functional/cli/cli_test.go b/tests/functional/cli/cli_test.go index 4580d49eb..e177c4be6 100644 --- a/tests/functional/cli/cli_test.go +++ b/tests/functional/cli/cli_test.go @@ -148,7 +148,7 @@ func TestNegativeCost(t *testing.T) { time.Sleep(100 * time.Millisecond) cmd.Process.Kill() - cmd.Process.Wait() + cmd.Wait() if receptorStdOut.String() != "Error: connection cost must be positive\n" { t.Fatalf("Expected stdout: Error: connection cost must be positive, actual stdout: %s", receptorStdOut.String()) } diff --git a/tests/functional/lib/mesh/climesh.go b/tests/functional/lib/mesh/climesh.go index 1c07d1f74..a6ea45799 100644 --- a/tests/functional/lib/mesh/climesh.go +++ b/tests/functional/lib/mesh/climesh.go @@ -10,6 +10,7 @@ import ( "path/filepath" "reflect" "strconv" + "sync" "time" "github.com/ansible/receptor/pkg/netceptor" @@ -18,10 +19,24 @@ import ( "gopkg.in/yaml.v2" ) +type Cmd struct { + *exec.Cmd + waitLock *sync.Mutex +} + +func (c *Cmd) WaitTS() error { + c.waitLock.Lock() + defer c.waitLock.Unlock() + + err := c.Wait() + + return err +} + // CLINode holds a Netceptor, this layer of abstraction might be unnecessary and // go away later. type CLINode struct { - receptorCmd *exec.Cmd + receptorCmd Cmd dir string yamlConfig []interface{} controlSocket string @@ -37,7 +52,6 @@ type CLIMesh struct { // NewCLINode builds a node with the name passed as the argument. func NewCLINode(name string) *CLINode { return &CLINode{ - receptorCmd: nil, controlSocket: "", } } @@ -82,12 +96,12 @@ func (n *CLINode) Start() error { } nodedefPath := filepath.Join(n.dir, "nodedef.yaml") ioutil.WriteFile(nodedefPath, strData, 0o644) - n.receptorCmd = exec.Command("receptor", "--config", nodedefPath) - stdout, err := os.Create(filepath.Join(n.dir, "stdout")) + n.receptorCmd = Cmd{exec.Command("receptor", "--config", nodedefPath), &sync.Mutex{}} + stdout, err := os.OpenFile(filepath.Join(n.dir, "stdout"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) if err != nil { return err } - stderr, err := os.Create(filepath.Join(n.dir, "stderr")) + stderr, err := os.OpenFile(filepath.Join(n.dir, "stderr"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) if err != nil { return err } @@ -103,7 +117,7 @@ func (n *CLINode) Start() error { func (n *CLINode) Destroy() { n.Shutdown() go func() { - n.receptorCmd.Wait() + n.receptorCmd.WaitTS() for _, i := range n.yamlConfig { m, ok := i.(map[interface{}]interface{}) if !ok { @@ -132,7 +146,7 @@ func (n *CLINode) Destroy() { // WaitForShutdown Waits for the receptor process to finish. func (n *CLINode) WaitForShutdown() { - n.receptorCmd.Wait() + n.receptorCmd.WaitTS() } // Dir returns the basedir which contains all of the mesh data. @@ -654,7 +668,7 @@ func (m *CLIMesh) CheckControlSockets() bool { // WaitForReady Waits for connections and routes to converge. func (m *CLIMesh) WaitForReady(ctx context.Context) error { - sleepInterval := 100 * time.Millisecond + sleepInterval := 500 * time.Millisecond if !utils.CheckUntilTimeout(ctx, sleepInterval, m.CheckControlSockets) { return errors.New("timed out while waiting for control sockets") } diff --git a/tests/functional/mesh/conn_test.go b/tests/functional/mesh/conn_test.go new file mode 100644 index 000000000..a421b7b34 --- /dev/null +++ b/tests/functional/mesh/conn_test.go @@ -0,0 +1,152 @@ +package mesh + +import ( + "context" + "fmt" + "io" + "net" + "strings" + "testing" + "time" + + "github.com/ansible/receptor/pkg/backends" + "github.com/ansible/receptor/pkg/netceptor" +) + +func TestQuicConnectTimeout(t *testing.T) { + // Change MaxIdleTimeoutForQuicConnections to 1 seconds (default in lib is 30, our code is 60) + netceptor.MaxIdleTimeoutForQuicConnections = 1 * time.Second + // We also have to disable heart beats or the connection will not properly timeout + netceptor.KeepAliveForQuicConnections = false + + // Create two nodes of the Receptor network-layer protocol (Netceptors). + n1 := netceptor.New(context.Background(), "node1") + n2 := netceptor.New(context.Background(), "node2") + + // Start a TCP listener on the first node + b1, err := backends.NewTCPListener("localhost:3333", nil) + if err != nil { + t.Fatal(fmt.Sprintf("Error listening on TCP: %s\n", err)) + } + err = n1.AddBackend(b1) + if err != nil { + t.Fatal(fmt.Sprintf("Error starting backend: %s\n", err)) + } + + // Start a TCP dialer on the second node - this will connect to the listener we just started + b2, err := backends.NewTCPDialer("localhost:3333", false, nil) + if err != nil { + t.Fatal(fmt.Sprintf("Error dialing on TCP: %s\n", err)) + } + err = n2.AddBackend(b2) + if err != nil { + t.Fatal(fmt.Sprintf("Error starting backend: %s\n", err)) + } + + // Start an echo server on node 1 + l1, err := n1.Listen("echo", nil) + if err != nil { + t.Fatal(fmt.Sprintf("Error listening on Receptor network: %s\n", err)) + } + go func() { + // Accept an incoming connection - note that conn is just a regular net.Conn + conn, err := l1.Accept() + if err != nil { + t.Fatal(fmt.Sprintf("Error accepting connection: %s\n", err)) + + return + } + go func() { + defer conn.Close() + buf := make([]byte, 1024) + done := false + for !done { + n, err := conn.Read(buf) + if err == io.EOF { + done = true + } else if err != nil { + // Is ok if we got a 'NO_ERROR: No recent network activity' error but anything else is a test failure. + if strings.Contains(err.Error(), "no recent network activity") { + t.Log("Successfully got the desired timeout error") + } else { + t.Fatal(fmt.Sprintf("Read error in Receptor listener: %s\n", err)) + } + + return + } + if n > 0 { + _, err := conn.Write(buf[:n]) + if err != nil { + t.Fatal(fmt.Sprintf("Write error in Receptor listener: %s\n", err)) + + return + } + } + } + }() + }() + + // Connect to the echo server from node 2. We expect this to error out at first with + // "no route to node" because it takes a second or two for node1 and node2 to exchange + // routing information and form a mesh. + var c2 net.Conn + for { + c2, err = n2.Dial("node1", "echo", nil) + if err != nil { + time.Sleep(1 * time.Second) + + continue + } + + break + } + + // Sleep longer than MaxIdleTimeout (see pkg/netceptor/conn.go for current setting) + sleepDuration := 6 * time.Second + time.Sleep(sleepDuration) + // Start a listener function that prints received data to the screen + // Note that because net.Conn is a stream connection, it is not guaranteed + // that received messages will be the same size as the messages that are sent. + // For datagram use, Receptor also provides a net.PacketConn. + go func() { + rbuf := make([]byte, 1024) + for { + n, err := c2.Read(rbuf) + if n > 0 { + n2.Shutdown() + t.Fatal("Should not have gotten data back") + + return + } + if err == io.EOF { + // Shut down the whole Netceptor when any connection closes, because this is just a demo + n2.Shutdown() + t.Fatal("Should not have gotten an EOF") + + return + } + if err != nil { + n2.Shutdown() + + return + } + } + }() + + // Send some data, which should be processed through the echo server back to our + // receive function and printed to the screen. + _, err = c2.Write([]byte("Hello, world!")) + if !(err != nil && err != io.EOF) { + t.Fatal("We should have gotten an error here") + } + + // Close our end of the connection + _ = c2.Close() + + // Wait for n2 to shut down + n2.BackendWait() + + // Gracefully shut down n1 + n1.Shutdown() + n1.BackendWait() +} diff --git a/tests/functional/mesh/mesh_test.go b/tests/functional/mesh/mesh_test.go index d8dcf2f76..b4b8faaff 100644 --- a/tests/functional/mesh/mesh_test.go +++ b/tests/functional/mesh/mesh_test.go @@ -49,25 +49,39 @@ func TestMeshStartup(t *testing.T) { t.Logf("waiting for mesh") ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) err = m.WaitForReady(ctx) + t.Logf("Mesh ready") if err != nil { t.Fatal(err) } // Test that each Node can ping each Node - for _, nodeSender := range m.Nodes() { + for nodeName, nodeSender := range m.Nodes() { + t.Logf("Pinging nodes from %s (%s)", nodeName, nodeSender.Dir()) controller := receptorcontrol.New() t.Logf("connecting to %s", nodeSender.ControlSocket()) err = controller.Connect(nodeSender.ControlSocket()) if err != nil { - t.Fatal(err) + t.Fatalf("Error connecting to controller: %s", err) } for nodeIDResponder := range m.Nodes() { t.Logf("pinging %s", nodeIDResponder) - response, err := controller.Ping(nodeIDResponder) - if err != nil { - t.Error(err) + retryloop: + for i := 30; i > 0; i-- { + response, err := controller.Ping(nodeIDResponder) + switch { + case err == nil: + t.Logf("%v", response) + + break retryloop + case i != 1: + t.Logf("Error pinging %s: %s. Retrying", nodeIDResponder, err) + + continue + default: + t.Fatalf("Error pinging %s: %s", nodeIDResponder, err) + } } - t.Logf("%v", response) } + t.Logf("All nodes connected") controller.Close() } }) @@ -211,6 +225,8 @@ func TestTraceroute(t *testing.T) { t.Fatal(fmt.Sprintf("hop %s should be %s but is actually %s", eh.key, eh.from, fromStr)) } } + + t.Logf("Finished %s\n", t.Name()) }) } } diff --git a/tests/functional/mesh/work_test.go b/tests/functional/mesh/work_test.go index 450314e21..504c2bb37 100644 --- a/tests/functional/mesh/work_test.go +++ b/tests/functional/mesh/work_test.go @@ -195,13 +195,17 @@ func TestWork(t *testing.T) { m, err := mesh.NewCLIMeshFromYaml(data, testName) if err != nil { - t.Fatal(err) + if m != nil { + t.Fatal(err, m.Dir()) + } else { + t.Fatal(err) + } } - ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes := m.Nodes() @@ -210,7 +214,7 @@ func TestWork(t *testing.T) { controller := receptorcontrol.New() err = controller.Connect(nodes[k].ControlSocket()) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } controllers[k] = controller } @@ -264,12 +268,12 @@ func TestWork(t *testing.T) { command := `{"command":"work","subcommand":"submit","worktype":"echosleepshort","tlsclient":"tlsclient","node":"node2","params":"", "ttl":"10h"}` unitID, err := controllers["node1"].WorkSubmitJSON(command) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ := context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkSucceeded(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) @@ -288,26 +292,26 @@ func TestWork(t *testing.T) { command := `{"command":"work","subcommand":"submit","worktype":"echosleepshort","tlsclient":"tlsclientwrongCN","node":"node2","params":""}` unitID, err := controllers["node1"].WorkSubmitJSON(command) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ := context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkFailed(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } _, err = controllers["node1"].WorkRelease(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 30*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkReleased(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = assertFilesReleased(ctx, nodes["node1"].Dir(), "node1", unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) @@ -326,26 +330,26 @@ func TestWork(t *testing.T) { command := `{"command":"work","subcommand":"submit","worktype":"echosleepshort","tlsclient":"tlsclient","node":"node2","params":"","ttl":"5s"}` unitID, err := controllers["node1"].WorkSubmitJSON(command) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ := context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkTimedOut(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } _, err = controllers["node1"].WorkRelease(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 30*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkReleased(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = assertFilesReleased(ctx, nodes["node1"].Dir(), "node1", unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) @@ -361,25 +365,25 @@ func TestWork(t *testing.T) { unitID, err := controllers["node1"].WorkSubmit("node3", "echosleeplong") if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ := context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkRunning(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } _, err = controllers["node1"].WorkCancel(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkCancelled(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } workStatus, err := controllers["node1"].GetWorkStatus(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } remoteUnitID := workStatus.ExtraData.(map[string]interface{})["RemoteUnitID"].(string) if remoteUnitID == "" { @@ -389,39 +393,39 @@ func TestWork(t *testing.T) { nodes["node1"].WaitForShutdown() err = nodes["node1"].Start() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 10*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node1"].Close() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node1"].Reconnect() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } _, err = controllers["node1"].WorkRelease(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 30*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkReleased(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 10*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = assertFilesReleased(ctx, nodes["node1"].Dir(), "node1", unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 10*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = assertFilesReleased(ctx, nodes["node3"].Dir(), "node3", remoteUnitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) @@ -438,27 +442,27 @@ func TestWork(t *testing.T) { nodes["node3"].WaitForShutdown() unitID, err := controllers["node1"].WorkSubmit("node3", "echosleepshort") if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkPending(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = nodes["node3"].Start() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // Wait for node3 to join the mesh again - ctx, _ = context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkSucceeded(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) @@ -473,44 +477,44 @@ func TestWork(t *testing.T) { unitID, err := controllers["node1"].WorkSubmit("node3", "echosleeplong") if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ := context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkRunning(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = assertStdoutFizeSize(ctx, nodes["node1"].Dir(), "node1", unitID, 1) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node1"].AssertWorkResults(unitID, expectedResults[:1]) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes["node2"].Shutdown() nodes["node2"].WaitForShutdown() nodes["node2"].Start() // Wait for node2 to join the mesh again - ctx, _ = context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 30*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkSucceeded(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 30*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = assertStdoutFizeSize(ctx, nodes["node1"].Dir(), "node1", unitID, 10) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node1"].AssertWorkResults(unitID, expectedResults) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) t.Run(testGroup+"/results on restarted node", func(t *testing.T) { @@ -524,33 +528,33 @@ func TestWork(t *testing.T) { unitID, err := controllers["node1"].WorkSubmit("node3", "echosleeplong") if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ := context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkRunning(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes["node3"].Shutdown() nodes["node3"].WaitForShutdown() err = nodes["node3"].Start() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // Wait for node3 to join the mesh again - ctx, _ = context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkSucceeded(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node1"].AssertWorkResults(unitID, expectedResults) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) t.Run(testGroup+"/work submit and release to non-existent node", func(t *testing.T) { @@ -566,45 +570,45 @@ func TestWork(t *testing.T) { // node999 was never initialised unitID, err := controllers["node1"].WorkSubmit("node999", "echosleeplong") if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // wait for 10 seconds, and check if the work is in pending state - ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkPending(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes["node1"].Shutdown() nodes["node1"].WaitForShutdown() err = nodes["node1"].Start() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 10*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node1"].Close() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node1"].Reconnect() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // release the work on node1 _, err = controllers["node1"].WorkRelease(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 30*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node1"].AssertWorkReleased(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) t.Run(testGroup+"/reload backends while streaming work results", func(t *testing.T) { @@ -618,14 +622,14 @@ func TestWork(t *testing.T) { // submit work from node 2 to node 3 unitID, err := controllers["node2"].WorkSubmit("node3", "echosleeplong50") if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // wait for 10 seconds, and check if the work is in pending state - ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node2"].AssertWorkPending(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // declare a new mesh with no connection between nodes @@ -644,14 +648,14 @@ func TestWork(t *testing.T) { // modify the existing mesh err = mesh.ModifyCLIMeshFromYaml(modifiedData, *m) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // reload the entire mesh for k := range controllers { err = controllers[k].Reload() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } } @@ -664,7 +668,7 @@ func TestWork(t *testing.T) { // read the work status workStatus, err := controllers["node2"].GetWorkStatus(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // modify the mesh to have connection again @@ -690,34 +694,34 @@ func TestWork(t *testing.T) { err = mesh.ModifyCLIMeshFromYaml(withConnectionData, *m) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // reload the entire mesh for k := range controllers { err = controllers[k].Reload() if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } } // wait for mesh to become ready again - ctx, _ = context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // ping should be successful in the mesh with connections _, err = controllers["node1"].Ping("node3") if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 15*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node2"].AssertWorkSizeIncreasing(ctx, unitID, workStatus.StdoutSize) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // it takes some time for the streaming to start again @@ -725,7 +729,7 @@ func TestWork(t *testing.T) { newWorkStatus, err := controllers["node2"].GetWorkStatus(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } if newWorkStatus.StdoutSize <= workStatus.StdoutSize { @@ -735,12 +739,12 @@ func TestWork(t *testing.T) { // cancel the work so that it doesnt run after the test ends _, err = controllers["node2"].WorkCancel(unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node2"].AssertWorkCancelled(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } }) } @@ -770,13 +774,17 @@ func TestRuntimeParams(t *testing.T) { m, err := mesh.NewCLIMeshFromYaml(data, t.Name()) if err != nil { - t.Fatal(err) + if m != nil { + t.Fatal(err, m.Dir()) + } else { + t.Fatal(err) + } } - ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes := m.Nodes() controllers := make(map[string]*receptorcontrol.ReceptorControl) @@ -784,21 +792,21 @@ func TestRuntimeParams(t *testing.T) { controllers["node0"] = receptorcontrol.New() err = controllers["node0"].Connect(nodes["node0"].ControlSocket()) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } command := `{"command":"work","subcommand":"submit","worktype":"echo","node":"node0","params":"it worked!"}` unitID, err := controllers["node0"].WorkSubmitJSON(command) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node0"].AssertWorkSucceeded(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node0"].AssertWorkResults(unitID, []byte("it worked!")) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } } @@ -837,12 +845,16 @@ func TestKubeRuntimeParams(t *testing.T) { } m, err := mesh.NewCLIMeshFromYaml(data, t.Name()) if err != nil { - t.Fatal(err) + if m != nil { + t.Fatal(err, m.Dir()) + } else { + t.Fatal(err) + } } - ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes := m.Nodes() controllers := make(map[string]*receptorcontrol.ReceptorControl) @@ -850,20 +862,20 @@ func TestKubeRuntimeParams(t *testing.T) { controllers["node0"] = receptorcontrol.New() err = controllers["node0"].Connect(nodes["node0"].ControlSocket()) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } command := fmt.Sprintf(`{"command": "work", "subcommand": "submit", "node": "localhost", "worktype": "echo", "secret_kube_pod": "---\napiVersion: v1\nkind: Pod\nspec:\n containers:\n - name: worker\n image: centos:8\n command:\n - bash\n args:\n - \"-c\"\n - for i in {1..5}; do echo $i;done\n", "secret_kube_config": "%s"}`, kubeconfig) unitID, err := controllers["node0"].WorkSubmitJSON(command) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node0"].AssertWorkSucceeded(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } err = controllers["node0"].AssertWorkResults(unitID, []byte("1\n2\n3\n4\n5\n")) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } } @@ -891,13 +903,17 @@ func TestRuntimeParamsNotAllowed(t *testing.T) { m, err := mesh.NewCLIMeshFromYaml(data, t.Name()) if err != nil { - t.Fatal(err) + if m != nil { + t.Fatal(err, m.Dir()) + } else { + t.Fatal(err) + } } - ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes := m.Nodes() controllers := make(map[string]*receptorcontrol.ReceptorControl) @@ -905,7 +921,7 @@ func TestRuntimeParamsNotAllowed(t *testing.T) { controllers["node0"] = receptorcontrol.New() err = controllers["node0"].Connect(nodes["node0"].ControlSocket()) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } command := `{"command":"work","subcommand":"submit","worktype":"echo","node":"node0","params":"it worked!"}` _, err = controllers["node0"].WorkSubmitJSON(command) @@ -941,13 +957,17 @@ func TestKubeContainerFailure(t *testing.T) { } m, err := mesh.NewCLIMeshFromYaml(data, t.Name()) if err != nil { - t.Fatal(err) + if m != nil { + t.Fatal(err, m.Dir()) + } else { + t.Fatal(err) + } } - ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes := m.Nodes() controllers := make(map[string]*receptorcontrol.ReceptorControl) @@ -955,14 +975,14 @@ func TestKubeContainerFailure(t *testing.T) { controllers["node0"] = receptorcontrol.New() err = controllers["node0"].Connect(nodes["node0"].ControlSocket()) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } job := `{"command":"work","subcommand":"submit","worktype":"kubejob","node":"node0"}` unitID, err := controllers["node0"].WorkSubmitJSON(job) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node0"].AssertWorkFailed(ctx, unitID) if err != nil { t.Fatal("Expected work to fail but it succeeded") @@ -1030,13 +1050,17 @@ func TestSignedWorkVerification(t *testing.T) { } m, err := mesh.NewCLIMeshFromYaml(data, t.Name()) if err != nil { - t.Fatal(err) + if m != nil { + t.Fatal(err, m.Dir()) + } else { + t.Fatal(err) + } } - ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) + ctx, _ := context.WithTimeout(context.Background(), 120*time.Second) err = m.WaitForReady(ctx) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } nodes := m.Nodes() controllers := make(map[string]*receptorcontrol.ReceptorControl) @@ -1044,18 +1068,18 @@ func TestSignedWorkVerification(t *testing.T) { controllers["node0"] = receptorcontrol.New() err = controllers["node0"].Connect(nodes["node0"].ControlSocket()) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } job := `{"command":"work","subcommand":"submit","worktype":"echo","node":"node1", "signwork":"true"}` unitID, err := controllers["node0"].WorkSubmitJSON(job) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } - ctx, _ = context.WithTimeout(context.Background(), 20*time.Second) + ctx, _ = context.WithTimeout(context.Background(), 120*time.Second) err = controllers["node0"].AssertWorkSucceeded(ctx, unitID) if err != nil { - t.Fatal(err) + t.Fatal(err, m.Dir()) } // node2 has the wrong public key to verify work signatures, so the work submission should fail diff --git a/tools/ansible/stage.yml b/tools/ansible/stage.yml new file mode 100644 index 000000000..2967c8046 --- /dev/null +++ b/tools/ansible/stage.yml @@ -0,0 +1,21 @@ +--- +- hosts: localhost + connection: local + vars: + payload: + name: "v{{ version }}" + tag_name: "v{{ version }}" + target_commitish: "{{ target_commitish }}" + draft: true + tasks: + - name: Publish draft Release + uri: + url: "https://api.github.com/repos/{{ repo }}/releases" + method: "POST" + headers: + Accept: "application/vnd.github.v3+json" + Authorization: "Bearer {{ github_token }}" + body: "{{ payload | to_json }}" + status_code: + - 200 + - 201