├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── buneary.go ├── cli.go ├── go.mod ├── go.sum ├── jobs.py ├── logo.png └── main.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | go-container: 5 | docker: 6 | - image: circleci/golang:1.15 7 | environment: 8 | GO111MODULE: "on" 9 | GOPROXY: "https://proxy.golang.org" 10 | 11 | jobs: 12 | # Run static Go-related checks, e.g. code formatting. 13 | go-checks: 14 | executor: go-container 15 | steps: 16 | - checkout 17 | - run: 18 | name: Install goimports tool 19 | command: | 20 | go get -u golang.org/x/tools/cmd/goimports 21 | echo "export PATH=$GOPATH/bin:$PATH" >> $BASH_ENV 22 | - run: 23 | name: Check Go format 24 | command: "! go fmt -l . | read" 25 | - run: 26 | name: Check Go imports 27 | command: "! goimports -l . | read" 28 | 29 | # Run all Go tests. 30 | go-test: 31 | executor: go-container 32 | steps: 33 | - checkout 34 | - restore_cache: 35 | keys: 36 | - gomodules-v1-{{ checksum "go.mod" }} 37 | - gomodules-v1- 38 | - run: 39 | name: Download dependencies 40 | command: go mod download 41 | - save_cache: 42 | key: gomodules-v1-{{ checksum "go.mod" }} 43 | paths: /go/pkg 44 | - run: 45 | name: Run all tests 46 | command: go test -v ./... 47 | 48 | # Verify that the requirements for creating a new release are met. 49 | pre-release-check: 50 | executor: go-container 51 | steps: 52 | - checkout 53 | - run: 54 | name: Check release in CHANGELOG.md 55 | command: python jobs.py check-changelog --tag=${CIRCLE_TAG} 56 | 57 | # Build buneary binaries for multiple platforms. 58 | build: 59 | executor: go-container 60 | steps: 61 | - checkout 62 | - restore_cache: 63 | keys: 64 | - gomodules-v1-{{ checksum "go.mod" }} 65 | - gomodules-v1- 66 | - run: 67 | name: Download dependencies 68 | command: go mod download 69 | - save_cache: 70 | key: gomodules-v1-{{ checksum "go.mod" }} 71 | paths: /go/pkg 72 | - run: mkdir -p /tmp/artifacts 73 | - run: 74 | name: Build and pack buneary for Linux 75 | command: | 76 | GOOS=linux GOARCH=amd64 go build \ 77 | -v \ 78 | -ldflags "-X main.version=${CIRCLE_TAG}" \ 79 | -o target/buneary . 80 | cp target/buneary buneary 81 | tar -czf /tmp/artifacts/buneary-linux-amd64.tar.gz buneary 82 | rm buneary 83 | - run: 84 | name: Build and pack buneary for macOS 85 | command: | 86 | GOOS=darwin GOARCH=amd64 go build \ 87 | -v \ 88 | -ldflags "-X main.version=${CIRCLE_TAG}" \ 89 | -o target/buneary . 90 | cp target/buneary buneary 91 | tar -czf /tmp/artifacts/buneary-darwin-amd64.tar.gz buneary 92 | rm buneary 93 | - run: 94 | name: Build and pack buneary for Windows 95 | command: | 96 | GOOS=windows GOARCH=amd64 go build \ 97 | -v \ 98 | -ldflags "-X main.version=${CIRCLE_TAG}" \ 99 | -o target/buneary.exe . 100 | cp target/buneary.exe buneary.exe 101 | zip /tmp/artifacts/buneary-windows-amd64.zip buneary.exe 102 | rm buneary.exe 103 | - persist_to_workspace: 104 | root: /tmp/artifacts 105 | paths: 106 | - buneary-* 107 | 108 | # Release the packaged binaries to GitHub. 109 | release-github: 110 | docker: 111 | - image: cibuilds/github:0.10 112 | steps: 113 | - checkout 114 | - attach_workspace: 115 | at: /tmp/artifacts 116 | - run: 117 | name: Install Python 118 | command: apk add --no-cache python2 119 | - run: 120 | name: Publish GitHub release 121 | command: | 122 | ghr -t "${GITHUB_TOKEN}" \ 123 | -u "${CIRCLE_PROJECT_USERNAME}" \ 124 | -r "${CIRCLE_PROJECT_REPONAME}" \ 125 | -c "${CIRCLE_SHA1}" \ 126 | -b "$(python jobs.py print-changelog --tag=${CIRCLE_TAG})" \ 127 | -delete "${CIRCLE_TAG}" \ 128 | /tmp/artifacts 129 | 130 | # Release the Docker images to Docker Hub and GitHub Packages. 131 | release-docker: 132 | docker: 133 | - image: circleci/buildpack-deps:stretch 134 | steps: 135 | - checkout 136 | - setup_remote_docker 137 | - run: 138 | name: Log in to Docker Hub 139 | command: | 140 | echo ${DOCKER_PASS} | docker login --username ${DOCKER_USER} --password-stdin 141 | - run: 142 | name: Build the distribution Docker image 143 | command: | 144 | docker image build --build-arg VERSION=${CIRCLE_TAG} -t dominikbraun/buneary:${CIRCLE_TAG} -f Dockerfile . 145 | - run: 146 | name: Tag the Docker images as latest 147 | command: | 148 | docker image tag dominikbraun/buneary:${CIRCLE_TAG} dominikbraun/buneary:latest 149 | - run: 150 | name: Publish the image on Docker Hub 151 | command: | 152 | docker image push dominikbraun/buneary:${CIRCLE_TAG} 153 | docker image push dominikbraun/buneary:latest 154 | - run: 155 | name: Log in to GitHub Packages 156 | command: | 157 | docker logout 158 | echo ${GITHUB_TOKEN} | docker login docker.pkg.github.com --username ${GITHUB_USER} --password-stdin 159 | - run: 160 | name: Tag the previously built Docker image 161 | command: | 162 | docker image tag dominikbraun/buneary:${CIRCLE_TAG} docker.pkg.github.com/dominikbraun/buneary/buneary:${CIRCLE_TAG} 163 | - run: 164 | name: Publish the image on GitHub Packages 165 | command: | 166 | docker image push docker.pkg.github.com/dominikbraun/buneary/buneary:${CIRCLE_TAG} 167 | 168 | workflows: 169 | version: 2 170 | # The basic CI workflow for single commits and opened PRs. 171 | buneary-ci: 172 | jobs: 173 | - go-checks 174 | - go-test: 175 | requires: 176 | - go-checks 177 | # The workflow for delivering the buneary application. 178 | buneary-cd: 179 | jobs: 180 | - pre-release-check: 181 | filters: 182 | tags: 183 | only: /v.*/ 184 | branches: 185 | ignore: /.*/ 186 | - build: 187 | requires: 188 | - pre-release-check 189 | filters: 190 | tags: 191 | only: /v.*/ 192 | - release-github: 193 | requires: 194 | - build 195 | filters: 196 | tags: 197 | only: /v.*/ 198 | - release-docker: 199 | requires: 200 | - release-github 201 | filters: 202 | tags: 203 | only: /v.*/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE directories. 2 | .idea/ 3 | .vscode/ 4 | 5 | # Build artifacts. 6 | target/ 7 | 8 | # Python virtual environment. 9 | venv/ 10 | 11 | # Binaries for programs and plugins. 12 | *.exe 13 | *.exe~ 14 | *.dll 15 | *.so 16 | *.dylib 17 | 18 | # Test binaries built with `go test -c`. 19 | *.test 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.3.1] - 2022-02-16 11 | 12 | ### Fixed 13 | - Fix constant requeuing of messages (#21). 14 | 15 | ## [0.3.0] - 2021-02-25 16 | 17 | ### Added 18 | - Add the `buneary get messages` command. 19 | 20 | ## [0.2.1] - 2021-02-19 21 | 22 | ### Changed 23 | - Print success messages on successful resource creations. 24 | 25 | ## [0.2.0] - 2021-02-10 26 | 27 | ### Added 28 | - Add the `buneary get exchanges` command. 29 | - Add the `buneary get exchange` command. 30 | - Add the `buneary get queues` command. 31 | - Add the `buneary get queue` command. 32 | - Add the `buneary get bindings` command. 33 | - Add the `buneary get binding` command. 34 | - Add the `--headers` option for specifying message headers. 35 | 36 | ### Changed 37 | - Use the HTTP API port `15672` instead of the AMQP port `5672`. 38 | 39 | ## [0.1.1] - 2021-02-07 40 | 41 | ### Changed 42 | - Enable support for `-v` flag, displaying version information. 43 | 44 | ### Fixed 45 | - Fix help text for `buneary create binding` command. 46 | 47 | ## [0.1.0] - 2021-02-05 48 | 49 | ### First `buneary` release. 50 | 51 | ## [0.0.0] 52 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile builds a lightweight distribution image for Docker Hub. 2 | # It only contains the application without any source code. 3 | FROM alpine:3.11.5 AS downloader 4 | 5 | # The buneary release to be downloaded from GitHub. 6 | ARG VERSION 7 | 8 | RUN apk add --no-cache \ 9 | curl \ 10 | tar 11 | 12 | RUN curl -LO https://github.com/dominikbraun/buneary/releases/download/${VERSION}/buneary-linux-amd64.tar.gz && \ 13 | tar -xvf buneary-linux-amd64.tar.gz -C /bin && \ 14 | rm -f buneary-linux-amd64.tar.gz 15 | 16 | # The final stage. This is the image that will be distrubuted. 17 | FROM alpine:3.11.5 AS final 18 | 19 | LABEL org.label-schema.schema-version="1.0" 20 | LABEL org.label-schema.name="buneary" 21 | LABEL org.label-schema.description="An easy-to-use CLI client for RabbitMQ." 22 | LABEL org.label-schema.url="https://github.com/dominikbraun/buneary" 23 | LABEL org.label-schema.vcs-url="https://github.com/dominikbraun/buneary" 24 | LABEL org.label-schema.version=${VERSION} 25 | 26 | COPY --from=downloader ["/bin/buneary", "/bin/buneary"] 27 | 28 | # Create a symlink for musl, see https://stackoverflow.com/a/35613430. 29 | RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 30 | 31 | WORKDIR /project 32 | 33 | ENTRYPOINT ["/bin/buneary"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

buneary 2 | 3 | 4 | 5 | 6 |

7 | 8 | `buneary`, pronounced _bun-ear-y_, is an easy-to-use RabbitMQ command line client for managing exchanges, managing 9 | queues, publishing messages to exchanges and reading messages from queues. 10 | 11 |

12 |
13 | buneary 14 |
15 |
16 |

17 | 18 | --- 19 | 20 | ## Contents 21 | 22 | * [Example](#example) 23 | * [Installation](#installation) 24 | * [macOS/Linux](#macoslinux) 25 | * [Windows](#windows) 26 | * [Docker](#docker) 27 | * [Usage](#usage) 28 | * [Create an exchange](#create-an-exchange) 29 | * [Create a queue](#create-a-queue) 30 | * [Create a binding](#create-a-binding) 31 | * [Get all exchanges](#get-all-exchanges) 32 | * [Get an exchange](#get-an-exchange) 33 | * [Get all queues](#get-all-queues) 34 | * [Get a queue](#get-a-queue) 35 | * [Get all bindings](#get-all-bindings) 36 | * [Get a binding](#get-a-binding) 37 | * [Get messages in a queue](#get-messages-in-a-queue) 38 | * [Publish a message](#publish-a-message) 39 | * [Delete an exchange](#delete-an-exchange) 40 | * [Delete a queue](#delete-a-queue) 41 | * [Credits](#credits) 42 | 43 | ## Example 44 | 45 | In the following example, a message `Hello!` is published and sent to an exchange called `my-exchange`. The RabbitMQ 46 | server is running on the local machine, and we'll use a routing key called `my-routing-key` for the message. 47 | 48 | ``` 49 | $ buneary publish localhost my-exchange my-routing-key "Hello!" 50 | ``` 51 | 52 | Since the RabbitMQ server listens to the default port, the port can be omitted here. The above command will prompt you 53 | to type in the username and password, but you could do this using command options as well. 54 | 55 | ## Installation 56 | 57 | ### macOS/Linux 58 | 59 | Download the [latest release](https://github.com/dominikbraun/buneary/releases) for your platform. Extract the 60 | downloaded binary into a directory like `/usr/local/bin`. Make sure the directory is in `PATH`. 61 | 62 | ### Windows 63 | 64 | Download the [latest release](https://github.com/dominikbraun/buneary/releases), create a directory like 65 | `C:\Program Files\buneary` and extract the executable into that directory. 66 | [Add the directory to `Path`](https://www.computerhope.com/issues/ch000549.htm). 67 | 68 | ### Docker 69 | 70 | Just append the actual `buneary` command you want to run after the image name. 71 | 72 | Because `buneary` needs to dial the RabbitMQ server, the Docker container needs to be in the same network as the 73 | RabbitMQ server. For example, if the server is running on your local machine, you could run a command as follows: 74 | 75 | ``` 76 | $ docker container run --network=host dominikbraun/buneary version 77 | ``` 78 | 79 | ## Usage 80 | 81 | ### Create an exchange 82 | 83 | **Syntax:** 84 | 85 | ``` 86 | $ buneary create exchange
[flags] 87 | ``` 88 | 89 | **Arguments:** 90 | 91 | |Argument|Description| 92 | |-|-| 93 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 94 | |`NAME`|The desired name of the new exchange.| 95 | |`TYPE`|The exchange type. Has to be one of `direct`, `headers`, `fanout` and `topic`.| 96 | 97 | **Flags:** 98 | 99 | |Flag|Short|Description| 100 | |-|-|-| 101 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 102 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 103 | |`--auto-delete`||Automatically delete the exchange once there are no bindings left.| 104 | |`--durable`||Make the exchange persistent, surviving server restarts.| 105 | |`--internal`||Make the exchange internal.| 106 | 107 | **Example:** 108 | 109 | Create a direct exchange called `my-exchange` on a RabbitMQ server running on the local machine. 110 | 111 | ``` 112 | $ buneary create exchange localhost my-exchange direct 113 | ``` 114 | 115 | ### Create a queue 116 | 117 | **Syntax:** 118 | 119 | ``` 120 | $ buneary create queue
[flags] 121 | ``` 122 | 123 | **Arguments:** 124 | 125 | |Argument|Description| 126 | |-|-| 127 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 128 | |`NAME`|The desired name of the new queue.| 129 | |`TYPE`|The queue type. Has to be one of `classic` and `quorum`.| 130 | 131 | **Flags:** 132 | 133 | |Flag|Short|Description| 134 | |-|-|-| 135 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 136 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 137 | |`--auto-delete`||Automatically delete the queue once there are no consumers left.| 138 | |`--durable`||Make the queue persistent, surviving server restarts.| 139 | 140 | **Example:** 141 | 142 | Create a classic queue called `my-queue` on a RabbitMQ server running on the local machine. 143 | 144 | ``` 145 | $ buneary create queue localhost my-queue classic 146 | ``` 147 | 148 | ### Create a binding 149 | 150 | **Syntax:** 151 | 152 | ``` 153 | $ buneary create binding
[flags] 154 | ``` 155 | 156 | **Arguments:** 157 | 158 | |Argument|Description| 159 | |-|-| 160 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 161 | |`NAME`|The desired name of the new binding.| 162 | |`TARGET`|The name of the target queue or exchange. If it is an exchange, use `--to-exchange`.| 163 | |`BINDING KEY`|The binding key.| 164 | 165 | **Flags:** 166 | 167 | |Flag|Short|Description| 168 | |-|-|-| 169 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 170 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 171 | |`--to-exchange`||Denote that the binding target is another exchange.| 172 | 173 | **Example:** 174 | 175 | Create a binding from `my-exchange` to `my-queue` on a RabbitMQ server running on the local machine. 176 | 177 | ``` 178 | $ buneary create binding localhost my-exchange my-queue my-binding-key 179 | ``` 180 | 181 | ### Get all exchanges 182 | 183 | **Syntax:** 184 | 185 | ``` 186 | $ buneary get exchanges
[flags] 187 | ``` 188 | 189 | **Arguments:** 190 | 191 | |Argument|Description| 192 | |-|-| 193 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 194 | 195 | **Flags:** 196 | 197 | |Flag|Short|Description| 198 | |-|-|-| 199 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 200 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 201 | 202 | **Example:** 203 | 204 | Get all exchanges from a RabbitMQ server running on the local machine - this particular example also shows the output. 205 | 206 | ``` 207 | $ buneary get exchanges localhost 208 | User: guest 209 | Password: 210 | +--------------------+---------+---------+-------------+----------+ 211 | | NAME | TYPE | DURABLE | AUTO-DELETE | INTERNAL | 212 | +--------------------+---------+---------+-------------+----------+ 213 | | | direct | yes | no | no | 214 | | amq.direct | direct | yes | no | no | 215 | | amq.fanout | fanout | yes | no | no | 216 | | amq.headers | headers | yes | no | no | 217 | | amq.match | headers | yes | no | no | 218 | | amq.rabbitmq.trace | topic | yes | no | yes | 219 | | amq.topic | topic | yes | no | no | 220 | +--------------------+---------+---------+-------------+----------+ 221 | 222 | ``` 223 | 224 | ### Get an exchange 225 | 226 | **Syntax:** 227 | 228 | ``` 229 | $ buneary get exchange
[flags] 230 | ``` 231 | 232 | **Arguments:** 233 | 234 | |Argument|Description| 235 | |-|-| 236 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 237 | |`NAME`|The name of the exchange.| 238 | 239 | **Flags:** 240 | 241 | |Flag|Short|Description| 242 | |-|-|-| 243 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 244 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 245 | 246 | **Example:** 247 | 248 | Get an exchange called `my-exchange` from a RabbitMQ server running on the local machine. 249 | 250 | ``` 251 | $ buneary get exchange localhost my-exchange 252 | ``` 253 | 254 | ### Get all queues 255 | 256 | **Syntax:** 257 | 258 | ``` 259 | $ buneary get queues
[flags] 260 | ``` 261 | 262 | **Arguments:** 263 | 264 | |Argument|Description| 265 | |-|-| 266 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 267 | 268 | **Flags:** 269 | 270 | |Flag|Short|Description| 271 | |-|-|-| 272 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 273 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 274 | 275 | **Example:** 276 | 277 | Get all queues from a RabbitMQ server running on the local machine. 278 | 279 | ``` 280 | $ buneary get queues localhost 281 | ``` 282 | 283 | ### Get a queue 284 | 285 | **Syntax:** 286 | 287 | ``` 288 | $ buneary get queue
[flags] 289 | ``` 290 | 291 | **Arguments:** 292 | 293 | |Argument|Description| 294 | |-|-| 295 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 296 | |`NAME`|The name of the queue.| 297 | 298 | **Flags:** 299 | 300 | |Flag|Short|Description| 301 | |-|-|-| 302 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 303 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 304 | 305 | **Example:** 306 | 307 | Get a queue called `my-queue` from a RabbitMQ server running on the local machine. 308 | 309 | ``` 310 | $ buneary get queue localhost my-exchange 311 | ``` 312 | 313 | ### Get all bindings 314 | 315 | **Syntax:** 316 | 317 | ``` 318 | $ buneary get bindings
[flags] 319 | ``` 320 | 321 | **Arguments:** 322 | 323 | |Argument|Description| 324 | |-|-| 325 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 326 | 327 | **Flags:** 328 | 329 | |Flag|Short|Description| 330 | |-|-|-| 331 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 332 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 333 | 334 | **Example:** 335 | 336 | Get all bindings from a RabbitMQ server running on the local machine. 337 | 338 | ``` 339 | $ buneary get bindings localhost 340 | ``` 341 | 342 | ### Get a binding 343 | 344 | **Syntax:** 345 | 346 | ``` 347 | $ buneary get binding
[flags] 348 | ``` 349 | 350 | **Arguments:** 351 | 352 | |Argument|Description| 353 | |-|-| 354 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 355 | |`EXCHANGE NAME`|The name of the source exchange.| 356 | |`TARGET NAME`|The name of the target.| 357 | 358 | **Flags:** 359 | 360 | |Flag|Short|Description| 361 | |-|-|-| 362 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 363 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 364 | 365 | **Example:** 366 | 367 | Get the binding or bindings between `my-exchange` and `my-queue` from a RabbitMQ server running on the local machine. 368 | 369 | ``` 370 | $ buneary get binding localhost my-exchange my-queue 371 | ``` 372 | 373 | ### Get messages in a queue 374 | 375 | **Syntax:** 376 | 377 | ``` 378 | $ buneary get messages
[flags] 379 | ``` 380 | 381 | **Arguments:** 382 | 383 | |Argument|Description| 384 | |-|-| 385 | |`ADDRESS`|The RabbitMQ AMQP address. If no port is specified, `5672` is used.| 386 | |`QUEUE NAME`|The name of the queue to read messages from.| 387 | 388 | **Flags:** 389 | 390 | |Flag|Short|Description| 391 | |-|-|-| 392 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 393 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 394 | |`--max`||The maximum amount of messages to read from the queue.| 395 | |`--requeue`||Reading messages will de-queue them. Re-queue the messages after reading them.| 396 | |`--force`|`-f`|Skip the manual confirmation and force reading the messages.| 397 | 398 | **Example:** 399 | 400 | Read up to 10 messages from the `my-queue` queue on a RabbitMQ server running on the local machine. 401 | 402 | ``` 403 | $ buneary get messages --max 10 localhost my-queue 404 | ``` 405 | 406 | ### Publish a message 407 | 408 | **Syntax:** 409 | 410 | ``` 411 | $ buneary publish
[flags] 412 | ``` 413 | 414 | **Arguments:** 415 | 416 | |Argument|Description| 417 | |-|-| 418 | |`ADDRESS`|The RabbitMQ AMQP address. If no port is specified, `5672` is used.| 419 | |`EXCHANGE`|The name of the target exchange.| 420 | |`ROUTING KEY`|The routing key of the message.| 421 | |`BODY`|The actual message body.| 422 | 423 | **Flags:** 424 | 425 | |Flag|Short|Description| 426 | |-|-|-| 427 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 428 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 429 | |`--headers`||Comma-separated message headers in the form `--headers key1=val1,key2=val2`.| 430 | 431 | **Example:** 432 | 433 | Publish a message `Hello!` to `my-exchange` on a RabbitMQ server running on the local machine. 434 | 435 | ``` 436 | $ buneary publish localhost my-exchange my-routing-key "Hello!" 437 | ``` 438 | 439 | ### Delete an exchange 440 | 441 | **Syntax:** 442 | 443 | ``` 444 | $ buneary delete exchange
[flags] 445 | ``` 446 | 447 | **Arguments:** 448 | 449 | |Argument|Description| 450 | |-|-| 451 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 452 | |`NAME`|The name of the exchange to be deleted.| 453 | 454 | **Flags:** 455 | 456 | |Flag|Short|Description| 457 | |-|-|-| 458 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 459 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 460 | 461 | **Example:** 462 | 463 | Delete an exchange called `my-exchange` on a RabbitMQ server running on the local machine. 464 | 465 | ``` 466 | $ buneary delete exchange localhost my-exchange 467 | ``` 468 | 469 | ### Delete a queue 470 | 471 | **Syntax:** 472 | 473 | ``` 474 | $ buneary delete queue
[flags] 475 | ``` 476 | 477 | **Arguments:** 478 | 479 | |Argument|Description| 480 | |-|-| 481 | |`ADDRESS`|The RabbitMQ HTTP API address. If no port is specified, `15672` is used.| 482 | |`NAME`|The name of the queue to be deleted.| 483 | 484 | **Flags:** 485 | 486 | |Flag|Short|Description| 487 | |-|-|-| 488 | |`--user`|`-u`|The username to connect with. If not specified, you will be asked for it.| 489 | |`--password`|`-p`|The password to authenticate with. If not specified, you will be asked for it.| 490 | 491 | **Example:** 492 | 493 | Delete a queue called `my-queue` on a RabbitMQ server running on the local machine. 494 | 495 | ``` 496 | $ buneary delete queue localhost my-queue 497 | ``` 498 | 499 | ## Credits 500 | 501 | * [michaelklishin/rabbit-hole](https://github.com/michaelklishin/rabbit-hole) is used as RabbitMQ client library. 502 | * [streadway/amqp](https://github.com/streadway/amqp) is used as AMQP client library. 503 | * For all third-party packages used, see [go.mod](go.mod). 504 | * The Buneary graphic is made by [dirocha](https://imgbin.com/user/dirocha). 505 | -------------------------------------------------------------------------------- /buneary.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | rabbithole "github.com/michaelklishin/rabbit-hole/v2" 13 | "github.com/streadway/amqp" 14 | ) 15 | 16 | const ( 17 | amqpDefaultPort = 5672 18 | apiDefaultPort = 15672 19 | ) 20 | 21 | type ( 22 | // ExchangeType represents the type of an exchange and thus defines its routing 23 | // behavior. The type cannot be changed after the exchange has been created. 24 | ExchangeType string 25 | 26 | // QueueType represents the type of a queue. 27 | QueueType string 28 | 29 | // BindingType represents the type of a binding and determines whether it binds 30 | // to a queue - which is the default case - or to another exchange. 31 | BindingType string 32 | ) 33 | 34 | const ( 35 | // Direct will deliver messages to queues based on their routing key. A direct 36 | // exchange compares the routing key to all registered binding keys and forwards 37 | // the message to all queues with matching binding keys. 38 | Direct ExchangeType = "direct" 39 | 40 | // Headers will deliver messages to queues based on their headers. This exchange 41 | // type will ignore the actual routing key. 42 | Headers = "headers" 43 | 44 | // Fanout will deliver messages to all bound queues of an exchange and ignore 45 | // the routing key, making them suitable for broadcasting scenarios. 46 | Fanout = "fanout" 47 | 48 | // Topic will deliver messages to queues based on a binding pattern. The exchange 49 | // will compare the routing key to all queue binding patterns and forward the 50 | // message to all matching queues. 51 | Topic = "topic" 52 | 53 | // Classic represents a classic message queue without any particularities. 54 | Classic QueueType = "classic" 55 | 56 | // Quorum represents a quorum queue. 57 | Quorum = "quorum" 58 | 59 | // ToQueue represents a binding from an exchange to a queue. 60 | ToQueue BindingType = "queue" 61 | 62 | // ToExchange represents a binding from an exchange to another exchange. 63 | ToExchange = "exchange" 64 | ) 65 | 66 | // Provider prescribes all functions a buneary implementation has to possess. 67 | type Provider interface { 68 | 69 | // CreateExchange creates a new exchange. If an exchange with the provided name 70 | // already exists, nothing will happen. 71 | CreateExchange(exchange Exchange) error 72 | 73 | // CreateQueue will create a new queue. If a queue with the provided name 74 | // already exists, nothing will happen. CreateQueue will return the queue 75 | // name generated by the server if no name has been provided. 76 | CreateQueue(queue Queue) (string, error) 77 | 78 | // CreateBinding will create a new binding. If a binding with the provided 79 | // target already exists, nothing will happen. 80 | CreateBinding(binding Binding) error 81 | 82 | // GetExchanges returns all exchanges that pass the provided filter function. 83 | // To get all exchanges, pass a filter function that always returns true. 84 | GetExchanges(filter func(exchange Exchange) bool) ([]Exchange, error) 85 | 86 | // GetQueues returns all queues that pass the provided filter function. To get 87 | // all queues, pass a filter function that always returns true. 88 | GetQueues(filter func(queue Queue) bool) ([]Queue, error) 89 | 90 | // GetBindings returns all bindings that pass the provided filter function. To 91 | // get all bindings, pass a filter function that always returns true. 92 | GetBindings(filter func(binding Binding) bool) ([]Binding, error) 93 | 94 | // GetMessages reads max messages from the given queue. The messages will be 95 | // re-queued if requeue is set to true. Otherwise, they will be removed from 96 | // the queue and thus won't be read by subscribers. 97 | // 98 | // This behavior may not be obvious to the user, especially if they merely 99 | // want to "take a look" into the queue without altering its state. Therefore, 100 | // an implementation should require the user opt-in to this behavior. 101 | GetMessages(queue Queue, max int, requeue bool) ([]Message, error) 102 | 103 | // PublishMessage publishes a message to the given exchange. The exchange 104 | // has to exist or must be created before the message is published. 105 | // 106 | // The actual message routing is defined by the exchange type. If no routing 107 | // key is given, the message will be sent to the default exchange. 108 | PublishMessage(message Message) error 109 | 110 | // DeleteExchange deletes the given exchange from the server. Will return 111 | // an error if the specified exchange name doesn't exist. 112 | DeleteExchange(exchange Exchange) error 113 | 114 | // DeleteQueue deletes the given queue from the server. Will return an error 115 | // if the specified queue name doesn't exist. 116 | DeleteQueue(queue Queue) error 117 | } 118 | 119 | // RabbitMQConfig stores RabbitMQ-related configuration values. 120 | type RabbitMQConfig struct { 121 | 122 | // Address specifies the RabbitMQ address in the form `localhost:5672`. The 123 | // port is not mandatory. If there's no port, 5672 will be used as default. 124 | Address string 125 | 126 | // User represents the username for setting up a connection. 127 | User string 128 | 129 | // Password represents the password to authenticate with. 130 | Password string 131 | } 132 | 133 | // URI returns the AMQP URI for a configuration, prefixed with amqp://. 134 | // In case the RabbitMQ address lacks a port, the default port will be used. 135 | func (a *RabbitMQConfig) URI() string { 136 | tokens := strings.Split(a.Address, ":") 137 | var port string 138 | 139 | if len(tokens) == 2 { 140 | port = tokens[1] 141 | } else { 142 | port = strconv.Itoa(amqpDefaultPort) 143 | } 144 | 145 | uri := fmt.Sprintf("amqp://%s:%s@%s:%s", a.User, a.Password, tokens[0], port) 146 | 147 | return uri 148 | } 149 | 150 | // apiURI returns the URI for the RabbitMQ HTTP API, prefixed with http://. In case 151 | // the RabbitMQ server address lacks a port, the default port will be used. 152 | func (a *RabbitMQConfig) apiURI() string { 153 | tokens := strings.Split(a.Address, ":") 154 | var port string 155 | 156 | if len(tokens) == 2 { 157 | port = tokens[1] 158 | } else { 159 | port = strconv.Itoa(apiDefaultPort) 160 | } 161 | 162 | uri := fmt.Sprintf("http://%s:%s", tokens[0], port) 163 | 164 | return uri 165 | } 166 | 167 | // Exchange represents a RabbitMQ exchange. 168 | type Exchange struct { 169 | 170 | // Name is the name of the exchange. Names starting with `amq.` denote pre- 171 | // defined exchanges and should be avoided. A valid name is not empty and only 172 | // contains letters, digits, hyphens, underscores, periods and colons. 173 | Name string 174 | 175 | // Type is the type of the exchange and determines in which fashion messages are 176 | // routed by the exchanged. It cannot be changed afterwards. 177 | Type ExchangeType 178 | 179 | // Durable determines whether the exchange will be persisted, i.e. be available 180 | // after server restarts. By default, an exchange is not durable. 181 | Durable bool 182 | 183 | // AutoDelete determines whether the exchange will be deleted automatically once 184 | // there are no bindings to any queues left. It won't be deleted by default. 185 | AutoDelete bool 186 | 187 | // Internal determines whether the exchange should be public-facing or not. 188 | Internal bool 189 | 190 | // NoWait determines whether the client should wait for the server confirming 191 | // operations related to the passed exchange. For instance, if NoWait is set to 192 | // false when creating an exchange, the client won't wait for confirmation. 193 | NoWait bool 194 | } 195 | 196 | // Queue represents a message queue. 197 | type Queue struct { 198 | 199 | // Name is the name of the queue. The name might be empty, in which case the 200 | // RabbitMQ server will generate and return a name for the queue. Queue names 201 | // follow the same rules as exchange names regarding the valid characters. 202 | Name string 203 | 204 | // Type is the type of the queue. Most users will only need classic queues, but 205 | // buneary strives to support quorum queues as well. 206 | // 207 | // For more information, see https://www.rabbitmq.com/quorum-queues.html. 208 | Type QueueType 209 | 210 | // Durable determines whether the queue will be persisted, i.e. be available after 211 | // server restarts. By default, an queue is not durable. 212 | Durable bool 213 | 214 | // AutoDelete determines whether the queue will be deleted automatically once 215 | // there are no consumers to ready from it left. It won't be deleted by default. 216 | AutoDelete bool 217 | } 218 | 219 | // Binding represents an exchange- or queue binding. 220 | type Binding struct { 221 | 222 | // Type is the type of the binding and determines whether the exchange binds to 223 | // another exchange or to a queue. Depending on the binding type, the server will 224 | // look for an exchange or queue with the provided target name. 225 | Type BindingType 226 | 227 | // From is the "source" of a binding going to the target. Even though this is an 228 | // Exchange instance, only the exchange name is needed for creating a binding. 229 | // 230 | // To bind to a durable queue, the source exchange has to be durable as well. This 231 | // won't be checked on client-side, but an error will be returned by the server if 232 | // this constraint is not met. 233 | From Exchange 234 | 235 | // TargetName is the name of the target, which is either an exchange or a queue. 236 | TargetName string 237 | 238 | // Key is the key of the binding. The key is crucial for message routing from the 239 | // exchange to the bound queue or to another exchange. 240 | Key string 241 | } 242 | 243 | // Message represents a message to be enqueued. 244 | type Message struct { 245 | 246 | // Target is the target exchange. Even though this is an entire Exchange instance, 247 | // only the exchange name is required for sending a message. 248 | Target Exchange 249 | 250 | // Headers represents the message headers, which is a set of arbitrary key-value 251 | // pairs. Message headers are considered by some exchange types and thus can be 252 | // relevant for message routing. 253 | Headers map[string]interface{} 254 | 255 | // RoutingKey is the routing key of the message and largely determines how the 256 | // message will be routed and which queues will receive the message. See the 257 | // individual ExchangeType constants for more information on routing behavior. 258 | RoutingKey string 259 | 260 | // Body represents the message body. 261 | Body []byte 262 | } 263 | 264 | // NewProvider initializes and returns a default Provider instance. 265 | func NewProvider(config *RabbitMQConfig) Provider { 266 | b := buneary{ 267 | config: config, 268 | } 269 | return &b 270 | } 271 | 272 | // buneary is an implementation of the Provider interface with sane defaults. 273 | type buneary struct { 274 | config *RabbitMQConfig 275 | channel *amqp.Channel 276 | client *rabbithole.Client 277 | } 278 | 279 | // setupChannel dials the configured RabbitMQ server, sets up a connection and opens a 280 | // channel from that connection, which should be closed once buneary has finished. 281 | func (b *buneary) setupChannel() error { 282 | if b.channel != nil { 283 | if err := b.channel.Close(); err != nil { 284 | return fmt.Errorf("closing AMQP channel: %w", err) 285 | } 286 | } 287 | 288 | conn, err := amqp.Dial(b.config.URI()) 289 | if err != nil { 290 | return fmt.Errorf("dialling RabbitMQ server: %w", err) 291 | } 292 | 293 | if b.channel, err = conn.Channel(); err != nil { 294 | return fmt.Errorf("establishing AMQP channel: %w", err) 295 | } 296 | 297 | return nil 298 | } 299 | 300 | // setupClient establishes a connection to the RabbitMQ HTTP API, initializing the 301 | // rabbit-hole client. It requires all connection data to exist in the configuration. 302 | func (b *buneary) setupClient() error { 303 | client, err := rabbithole.NewClient(b.config.apiURI(), b.config.User, b.config.Password) 304 | if err != nil { 305 | return fmt.Errorf("creating rabbit-hole client: %w", err) 306 | } 307 | b.client = client 308 | 309 | return nil 310 | } 311 | 312 | // CreateExchange creates the given exchange. See Provider.CreateExchange for details. 313 | func (b *buneary) CreateExchange(exchange Exchange) error { 314 | if err := b.setupClient(); err != nil { 315 | return err 316 | } 317 | 318 | _, err := b.client.DeclareExchange("/", exchange.Name, rabbithole.ExchangeSettings{ 319 | Type: string(exchange.Type), 320 | Durable: exchange.Durable, 321 | AutoDelete: exchange.AutoDelete, 322 | }) 323 | if err != nil { 324 | return fmt.Errorf("declaring exchange: %w", err) 325 | } 326 | 327 | return nil 328 | } 329 | 330 | // CreateQueue creates the given queue. See Provider.CreateQueue for details. 331 | func (b *buneary) CreateQueue(queue Queue) (string, error) { 332 | if err := b.setupClient(); err != nil { 333 | return "", err 334 | } 335 | 336 | // ToDo: Fetch and return the generated queue name from the response. 337 | _, err := b.client.DeclareQueue("/", queue.Name, rabbithole.QueueSettings{ 338 | Type: string(queue.Type), 339 | Durable: queue.Durable, 340 | AutoDelete: queue.AutoDelete, 341 | }) 342 | if err != nil { 343 | return "", fmt.Errorf("declaring queue: %w", err) 344 | } 345 | 346 | return "", nil 347 | } 348 | 349 | // CreateBinding creates the given binding. See Provider.CreateBinding for details. 350 | func (b *buneary) CreateBinding(binding Binding) error { 351 | if err := b.setupClient(); err != nil { 352 | return err 353 | } 354 | 355 | _, err := b.client.DeclareBinding("/", rabbithole.BindingInfo{ 356 | Source: binding.From.Name, 357 | Vhost: "/", 358 | Destination: binding.TargetName, 359 | DestinationType: string(binding.Type), 360 | RoutingKey: binding.Key, 361 | }) 362 | if err != nil { 363 | return fmt.Errorf("declaring binding: %w", err) 364 | } 365 | 366 | return nil 367 | } 368 | 369 | // GetExchanges returns exchanges passing the filter. See Provider.GetExchanges for details. 370 | func (b *buneary) GetExchanges(filter func(exchange Exchange) bool) ([]Exchange, error) { 371 | if err := b.setupClient(); err != nil { 372 | return nil, err 373 | } 374 | 375 | exchangeInfos, err := b.client.ListExchanges() 376 | if err != nil { 377 | return nil, fmt.Errorf("listing exchanges: %w", err) 378 | } 379 | 380 | var exchanges []Exchange 381 | 382 | for _, info := range exchangeInfos { 383 | e := Exchange{ 384 | Name: info.Name, 385 | Type: ExchangeType(info.Type), 386 | Durable: info.Durable, 387 | AutoDelete: info.AutoDelete, 388 | Internal: info.Internal, 389 | } 390 | 391 | if filter(e) { 392 | exchanges = append(exchanges, e) 393 | } 394 | } 395 | 396 | return exchanges, nil 397 | } 398 | 399 | // GetQueues returns queues passing the filter. See Provider.GetQueues for details. 400 | func (b *buneary) GetQueues(filter func(queue Queue) bool) ([]Queue, error) { 401 | if err := b.setupClient(); err != nil { 402 | return nil, err 403 | } 404 | 405 | queueInfos, err := b.client.ListQueues() 406 | if err != nil { 407 | return nil, fmt.Errorf("listing queues: %w", err) 408 | } 409 | 410 | var queues []Queue 411 | 412 | for _, info := range queueInfos { 413 | q := Queue{ 414 | Name: info.Name, 415 | Durable: info.Durable, 416 | AutoDelete: info.AutoDelete, 417 | } 418 | 419 | if filter(q) { 420 | queues = append(queues, q) 421 | } 422 | } 423 | 424 | return queues, nil 425 | } 426 | 427 | // GetBindings returns bindings passing the filter. See Provider.GetBindings for details. 428 | func (b *buneary) GetBindings(filter func(binding Binding) bool) ([]Binding, error) { 429 | if err := b.setupClient(); err != nil { 430 | return nil, err 431 | } 432 | 433 | bindingInfos, err := b.client.ListBindings() 434 | if err != nil { 435 | return nil, fmt.Errorf("listing bindings: %w", err) 436 | } 437 | 438 | var bindings []Binding 439 | 440 | for _, info := range bindingInfos { 441 | b := Binding{ 442 | Type: BindingType(info.DestinationType), 443 | From: Exchange{Name: info.Source}, 444 | TargetName: info.Destination, 445 | Key: info.RoutingKey, 446 | } 447 | 448 | if filter(b) { 449 | bindings = append(bindings, b) 450 | } 451 | } 452 | 453 | return bindings, nil 454 | } 455 | 456 | // GetMessages reads messages from the given queue. See Provider.GetMessages for details. 457 | // 458 | // ToDo: Maybe move the function-scoped types somewhere else. 459 | func (b *buneary) GetMessages(queue Queue, max int, requeue bool) ([]Message, error) { 460 | // getMessagesRequestBody represents the HTTP request body for reading messages. 461 | type getMessagesRequestBody struct { 462 | Count int `json:"count"` 463 | Encoding string `json:"encoding"` 464 | Ackmode string `json:"ackmode"` 465 | } 466 | 467 | // getMessagesRequestBody represents the HTTP response body returned by the RabbitMQ 468 | // API endpoint for reading messages from a queue (/api/queues/vhost/name/get). 469 | type getMessagesResponseBody []struct { 470 | PayloadBytes int `json:"payload_bytes"` 471 | Redelivered bool `json:"redelivered"` 472 | Exchange string `json:"exchange"` 473 | RoutingKey string `json:"routing_key"` 474 | Headers map[string]interface{} `json:"headers"` 475 | Payload string `json:"payload"` 476 | } 477 | 478 | ackMode := "ack_requeue_false" 479 | if requeue { 480 | ackMode = "ack_requeue_true" 481 | } 482 | 483 | requestBody := getMessagesRequestBody{ 484 | Count: max, 485 | Encoding: "auto", 486 | Ackmode: ackMode, 487 | } 488 | 489 | requestBodyJson, err := json.Marshal(requestBody) 490 | if err != nil { 491 | return nil, fmt.Errorf("marshalling request body: %w", err) 492 | } 493 | 494 | uri := fmt.Sprintf("%s/api/queues/%%2F/%s/get", b.config.apiURI(), queue.Name) 495 | 496 | request, err := http.NewRequest("POST", uri, bytes.NewReader(requestBodyJson)) 497 | if err != nil { 498 | return nil, fmt.Errorf("creating POST request: %w", err) 499 | } 500 | 501 | request.SetBasicAuth(b.config.User, b.config.Password) 502 | 503 | response, err := (&http.Client{}).Do(request) 504 | if err != nil { 505 | return nil, err 506 | } 507 | 508 | if response.StatusCode != 200 { 509 | return nil, fmt.Errorf("RabbitMQ server returned non-200 status: %s", response.Status) 510 | } 511 | 512 | defer func() { 513 | _ = response.Body.Close() 514 | }() 515 | 516 | responseBody := getMessagesResponseBody{} 517 | 518 | if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil { 519 | return nil, err 520 | } 521 | 522 | messages := make([]Message, len(responseBody)) 523 | 524 | for i, m := range responseBody { 525 | messages[i] = Message{ 526 | Target: Exchange{Name: m.Exchange}, 527 | Headers: m.Headers, 528 | RoutingKey: m.RoutingKey, 529 | Body: []byte(m.Payload), 530 | } 531 | } 532 | 533 | return messages, nil 534 | } 535 | 536 | // PublishMessage publishes the given message. See Provider.PublishMessage for details. 537 | func (b *buneary) PublishMessage(message Message) error { 538 | if err := b.setupChannel(); err != nil { 539 | return err 540 | } 541 | 542 | defer func() { 543 | _ = b.Close() 544 | }() 545 | 546 | if err := b.channel.Publish(messageArgs(message)); err != nil { 547 | return fmt.Errorf("publishing message: %w", err) 548 | } 549 | 550 | return nil 551 | } 552 | 553 | // DeleteExchange deletes the given exchange. See Provider.DeleteExchange for details. 554 | func (b *buneary) DeleteExchange(exchange Exchange) error { 555 | if err := b.setupClient(); err != nil { 556 | return err 557 | } 558 | 559 | _, err := b.client.DeleteExchange("/", exchange.Name) 560 | if err != nil { 561 | return fmt.Errorf("deleting exchange: %w", err) 562 | } 563 | 564 | return nil 565 | } 566 | 567 | // DeleteQueue deletes the given exchange. See Provider.DeleteQueue for details. 568 | func (b *buneary) DeleteQueue(queue Queue) error { 569 | if err := b.setupClient(); err != nil { 570 | return err 571 | } 572 | 573 | _, err := b.client.DeleteQueue("/", queue.Name) 574 | if err != nil { 575 | return fmt.Errorf("deleting queue: %w", err) 576 | } 577 | 578 | return nil 579 | } 580 | 581 | // Close closes the AMQP channel to the configured RabbitMQ server. This function 582 | // should be called after running PublishMessage. 583 | func (b *buneary) Close() error { 584 | if b.channel != nil { 585 | if err := b.channel.Close(); err != nil { 586 | return fmt.Errorf("closing AMQP channel: %w", err) 587 | } 588 | } 589 | 590 | return nil 591 | } 592 | 593 | // messageArgs returns all message fields expected by the AMQP library as single 594 | // values. This avoids large parameter lists when calling library functions. 595 | func messageArgs(message Message) (string, string, bool, bool, amqp.Publishing) { 596 | return message.Target.Name, 597 | message.RoutingKey, 598 | false, 599 | false, 600 | amqp.Publishing{ 601 | Headers: message.Headers, 602 | Timestamp: time.Now(), 603 | Body: message.Body, 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | 13 | "github.com/olekukonko/tablewriter" 14 | "github.com/spf13/cobra" 15 | "golang.org/x/crypto/ssh/terminal" 16 | ) 17 | 18 | var version = "UNDEFINED" 19 | 20 | // globalOptions defines global command line options available for all commands. 21 | // They're read by the top-level command and passed to the sub-command factories. 22 | type globalOptions struct { 23 | user string 24 | password string 25 | out io.StringWriter 26 | } 27 | 28 | // rootCommand creates the top-level `buneary` command without any functionality. 29 | func rootCommand() *cobra.Command { 30 | options := globalOptions{ 31 | out: os.Stdout, 32 | } 33 | 34 | root := &cobra.Command{ 35 | Use: "buneary", 36 | Short: "An easy-to-use CLI client for RabbitMQ.", 37 | Long: `buneary, pronounced bun-ear-y, is an easy-to-use RabbitMQ command line client 38 | for managing exchanges, managing queues, publishing messages to exchanges and reading messages from queues.`, 39 | Version: version, 40 | SilenceUsage: true, 41 | SilenceErrors: true, 42 | RunE: func(cmd *cobra.Command, args []string) error { 43 | return nil 44 | }, 45 | } 46 | 47 | root.AddCommand(createCommand(&options)) 48 | root.AddCommand(getCommand(&options)) 49 | root.AddCommand(publishCommand(&options)) 50 | root.AddCommand(deleteCommand(&options)) 51 | root.AddCommand(versionCommand(&options)) 52 | 53 | root.PersistentFlags(). 54 | StringVarP(&options.user, "user", "u", "", "the username to connect with") 55 | root.PersistentFlags(). 56 | StringVarP(&options.password, "password", "p", "", "the password to authenticate with") 57 | 58 | return root 59 | } 60 | 61 | // createCommand creates the `buneary create` command without any functionality. 62 | func createCommand(options *globalOptions) *cobra.Command { 63 | create := &cobra.Command{ 64 | Use: "create ", 65 | Short: "Create a resource", 66 | RunE: func(cmd *cobra.Command, args []string) error { 67 | return cmd.Help() 68 | }, 69 | } 70 | 71 | create.AddCommand(createExchangeCommand(options)) 72 | create.AddCommand(createQueueCommand(options)) 73 | create.AddCommand(createBindingCommand(options)) 74 | 75 | return create 76 | } 77 | 78 | // createExchangeOptions defines options for creating a new exchange. 79 | type createExchangeOptions struct { 80 | *globalOptions 81 | durable bool 82 | autoDelete bool 83 | internal bool 84 | noWait bool 85 | } 86 | 87 | // createExchangeCommand creates the `buneary create exchange` command, making sure 88 | // that exactly three arguments are passed. 89 | // 90 | // At the moment, there is no support for setting Exchange.NoWait via this command. 91 | func createExchangeCommand(options *globalOptions) *cobra.Command { 92 | createExchangeOptions := &createExchangeOptions{ 93 | globalOptions: options, 94 | } 95 | 96 | createExchange := &cobra.Command{ 97 | Use: "exchange
", 98 | Short: "Create a new exchange", 99 | Args: cobra.ExactArgs(3), 100 | RunE: func(cmd *cobra.Command, args []string) error { 101 | return runCreateExchange(createExchangeOptions, args) 102 | }, 103 | } 104 | 105 | createExchange.Flags(). 106 | BoolVar(&createExchangeOptions.durable, "durable", false, "make the exchange durable") 107 | createExchange.Flags(). 108 | BoolVar(&createExchangeOptions.autoDelete, "auto-delete", false, "make the exchange auto-deleted") 109 | createExchange.Flags(). 110 | BoolVar(&createExchangeOptions.internal, "internal", false, "make the exchange internal") 111 | 112 | return createExchange 113 | } 114 | 115 | // runCreateExchange creates a new exchange by reading the command line data, setting 116 | // the configuration and calling the runCreateExchange function. In case the password 117 | // or both the user and password aren't provided, it will go into interactive mode. 118 | // 119 | // ToDo: Move the logic for parsing the exchange type into Exchange. 120 | func runCreateExchange(options *createExchangeOptions, args []string) error { 121 | var ( 122 | address = args[0] 123 | name = args[1] 124 | exchangeType = args[2] 125 | ) 126 | 127 | user, password := getOrReadInCredentials(options.globalOptions) 128 | 129 | provider := NewProvider(&RabbitMQConfig{ 130 | Address: address, 131 | User: user, 132 | Password: password, 133 | }) 134 | 135 | exchange := Exchange{ 136 | Name: name, 137 | Durable: options.durable, 138 | AutoDelete: options.autoDelete, 139 | Internal: options.internal, 140 | NoWait: options.noWait, 141 | } 142 | 143 | switch exchangeType { 144 | case "direct": 145 | exchange.Type = Direct 146 | case "headers": 147 | exchange.Type = Headers 148 | case "fanout": 149 | exchange.Type = Fanout 150 | case "topic": 151 | exchange.Type = Topic 152 | } 153 | 154 | if err := provider.CreateExchange(exchange); err != nil { 155 | return err 156 | } 157 | 158 | _, _ = options.out.WriteString("exchange created successfully\n") 159 | 160 | return nil 161 | } 162 | 163 | // createQueueOptions defines options for creating a new queue. 164 | type createQueueOptions struct { 165 | *globalOptions 166 | durable bool 167 | autoDelete bool 168 | } 169 | 170 | // createQueueCommand creates the `buneary create queue` command, making sure that 171 | // exactly three arguments are passed. 172 | // 173 | // The argument may become optional for convenience in the future. In this 174 | // case, it should default to the classic queue type. 175 | func createQueueCommand(options *globalOptions) *cobra.Command { 176 | createQueueOptions := &createQueueOptions{ 177 | globalOptions: options, 178 | } 179 | 180 | createQueue := &cobra.Command{ 181 | Use: "queue
", 182 | Short: "Create a new queue", 183 | Args: cobra.ExactArgs(3), 184 | RunE: func(cmd *cobra.Command, args []string) error { 185 | return runCreateQueue(createQueueOptions, args) 186 | }, 187 | } 188 | 189 | createQueue.Flags(). 190 | BoolVar(&createQueueOptions.durable, "durable", false, "make the queue durable") 191 | createQueue.Flags(). 192 | BoolVar(&createQueueOptions.autoDelete, "auto-delete", false, "make the queue auto-deleted") 193 | 194 | return createQueue 195 | } 196 | 197 | // runCreateQueue creates a new queue by reading the command line data, setting the 198 | // configuration and calling the CreateQueue function. In case the password or both 199 | // the user and password aren't provided, it will go into interactive mode. 200 | // 201 | // If the queue type is empty or invalid, the queue type defaults to Classic. 202 | func runCreateQueue(options *createQueueOptions, args []string) error { 203 | var ( 204 | address = args[0] 205 | name = args[1] 206 | queueType = args[2] 207 | ) 208 | 209 | user, password := getOrReadInCredentials(options.globalOptions) 210 | 211 | provider := NewProvider(&RabbitMQConfig{ 212 | Address: address, 213 | User: user, 214 | Password: password, 215 | }) 216 | 217 | queue := Queue{ 218 | Name: name, 219 | Durable: options.durable, 220 | AutoDelete: options.autoDelete, 221 | } 222 | 223 | switch queueType { 224 | case "quorum": 225 | queue.Type = Quorum 226 | case "classic": 227 | fallthrough 228 | default: 229 | queue.Type = Classic 230 | } 231 | 232 | _, err := provider.CreateQueue(queue) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | _, _ = options.out.WriteString("queue created successfully\n") 238 | 239 | return nil 240 | } 241 | 242 | // createBindingOptions defines options for creating a new binding. 243 | type createBindingOptions struct { 244 | *globalOptions 245 | toExchange bool 246 | } 247 | 248 | // createBindingCommand creates the `buneary create binding` command, making sure 249 | // that exactly four arguments are passed. 250 | func createBindingCommand(options *globalOptions) *cobra.Command { 251 | createBindingOptions := &createBindingOptions{ 252 | globalOptions: options, 253 | } 254 | 255 | createQueue := &cobra.Command{ 256 | Use: "binding
", 257 | Short: "Create a new binding", 258 | Args: cobra.ExactArgs(4), 259 | RunE: func(cmd *cobra.Command, args []string) error { 260 | return runCreateBinding(createBindingOptions, args) 261 | }, 262 | } 263 | 264 | createQueue.Flags(). 265 | BoolVar(&createBindingOptions.toExchange, "to-exchange", false, "the target is another exchange") 266 | 267 | return createQueue 268 | } 269 | 270 | // runCreateBinding creates a new binding by reading the command line data, setting 271 | // the configuration and calling the CreateQueue function. In case the password or 272 | // both the user and password aren't provided, it will go into interactive mode. 273 | // 274 | // The binding type defaults to ToQueue. To create a binding to another exchange, the 275 | // --to-exchange flag has to be used. 276 | func runCreateBinding(options *createBindingOptions, args []string) error { 277 | var ( 278 | address = args[0] 279 | name = args[1] 280 | target = args[2] 281 | bindingKey = args[3] 282 | ) 283 | 284 | user, password := getOrReadInCredentials(options.globalOptions) 285 | 286 | provider := NewProvider(&RabbitMQConfig{ 287 | Address: address, 288 | User: user, 289 | Password: password, 290 | }) 291 | 292 | binding := Binding{ 293 | From: Exchange{Name: name}, 294 | TargetName: target, 295 | Key: bindingKey, 296 | } 297 | 298 | switch options.toExchange { 299 | case true: 300 | binding.Type = ToExchange 301 | default: 302 | binding.Type = ToQueue 303 | } 304 | 305 | if err := provider.CreateBinding(binding); err != nil { 306 | return err 307 | } 308 | 309 | _, _ = options.out.WriteString("queue created successfully\n") 310 | 311 | return nil 312 | } 313 | 314 | // getCommand creates the `buneary get` command without any functionality. 315 | func getCommand(options *globalOptions) *cobra.Command { 316 | get := &cobra.Command{ 317 | Use: "get ", 318 | Short: "Get a resource", 319 | RunE: func(cmd *cobra.Command, args []string) error { 320 | return cmd.Help() 321 | }, 322 | } 323 | 324 | get.AddCommand(getExchangesCommand(options)) 325 | get.AddCommand(getExchangeCommand(options)) 326 | get.AddCommand(getQueuesCommand(options)) 327 | get.AddCommand(getQueueCommand(options)) 328 | get.AddCommand(getBindingsCommand(options)) 329 | get.AddCommand(getBindingCommand(options)) 330 | get.AddCommand(getMessagesCommand(options)) 331 | 332 | return get 333 | } 334 | 335 | // getExchangesCommand creates the `buneary get exchanges` command, making sure that 336 | // exactly one argument is passed. 337 | func getExchangesCommand(options *globalOptions) *cobra.Command { 338 | getExchanges := &cobra.Command{ 339 | Use: "exchanges
", 340 | Short: "Get all available exchanges", 341 | Args: cobra.ExactArgs(1), 342 | RunE: func(cmd *cobra.Command, args []string) error { 343 | return runGetExchanges(options, args) 344 | }, 345 | } 346 | 347 | return getExchanges 348 | } 349 | 350 | // getExchangeCommand creates the `buneary get exchange` command, making sure that exactly 351 | // two arguments are passed. 352 | func getExchangeCommand(options *globalOptions) *cobra.Command { 353 | getExchange := &cobra.Command{ 354 | Use: "exchange
", 355 | Short: "Get a single exchange", 356 | Args: cobra.ExactArgs(2), 357 | RunE: func(cmd *cobra.Command, args []string) error { 358 | return runGetExchanges(options, args) 359 | }, 360 | } 361 | 362 | return getExchange 363 | } 364 | 365 | // runGetExchanges either returns all exchanges or - if an exchange name has been 366 | // specified as second argument - a single exchange. In case the password or both 367 | // the user and password aren't provided, it will go into interactive mode. 368 | // 369 | // This flexibility allows runGetExchanges to be used by both `buneary get exchanges` 370 | // as well as `buneary get exchange`. 371 | func runGetExchanges(options *globalOptions, args []string) error { 372 | var ( 373 | address = args[0] 374 | ) 375 | 376 | user, password := getOrReadInCredentials(options) 377 | 378 | provider := NewProvider(&RabbitMQConfig{ 379 | Address: address, 380 | User: user, 381 | Password: password, 382 | }) 383 | 384 | // The default filter will let pass all exchanges regardless of their names. 385 | filter := func(_ Exchange) bool { 386 | return true 387 | } 388 | 389 | // However, if an exchange name has been specified as second argument, only 390 | // that particular exchange should be returned. 391 | if len(args) > 1 { 392 | filter = func(exchange Exchange) bool { 393 | return exchange.Name == args[1] 394 | } 395 | } 396 | 397 | exchanges, err := provider.GetExchanges(filter) 398 | if err != nil { 399 | return err 400 | } 401 | 402 | table := tablewriter.NewWriter(os.Stdout) 403 | table.SetHeader([]string{"Name", "Type", "Durable", "Auto-Delete", "Internal"}) 404 | 405 | for _, exchange := range exchanges { 406 | row := make([]string, 5) 407 | row[0] = exchange.Name 408 | row[1] = string(exchange.Type) 409 | row[2] = boolToString(exchange.Durable) 410 | row[3] = boolToString(exchange.AutoDelete) 411 | row[4] = boolToString(exchange.Internal) 412 | table.Append(row) 413 | } 414 | 415 | table.Render() 416 | 417 | return nil 418 | } 419 | 420 | // getQueuesCommand creates the `buneary get queues` command, making sure that 421 | // exactly one argument is passed. 422 | func getQueuesCommand(options *globalOptions) *cobra.Command { 423 | getQueues := &cobra.Command{ 424 | Use: "queues
", 425 | Short: "Get all available queues", 426 | Args: cobra.ExactArgs(1), 427 | RunE: func(cmd *cobra.Command, args []string) error { 428 | return runGetQueues(options, args) 429 | }, 430 | } 431 | 432 | return getQueues 433 | } 434 | 435 | // getQueueCommand creates the `buneary get queue` command, making sure that exactly two 436 | // arguments are passed. 437 | func getQueueCommand(options *globalOptions) *cobra.Command { 438 | getQueue := &cobra.Command{ 439 | Use: "queue
", 440 | Short: "Get a single queue", 441 | Args: cobra.ExactArgs(2), 442 | RunE: func(cmd *cobra.Command, args []string) error { 443 | return runGetQueues(options, args) 444 | }, 445 | } 446 | 447 | return getQueue 448 | } 449 | 450 | // runGetQueues either returns all queues or - if a queue name has been specified as second 451 | // argument - a single queue. In case the password or both the user and password aren't 452 | // provided, it will go into interactive mode. 453 | // 454 | // This flexibility allows runGetQueues to be used by both `buneary get queues` as well as 455 | // `buneary get queue`. 456 | func runGetQueues(options *globalOptions, args []string) error { 457 | var ( 458 | address = args[0] 459 | ) 460 | 461 | user, password := getOrReadInCredentials(options) 462 | 463 | provider := NewProvider(&RabbitMQConfig{ 464 | Address: address, 465 | User: user, 466 | Password: password, 467 | }) 468 | 469 | // The default filter will let pass all queues regardless of their names. 470 | filter := func(_ Queue) bool { 471 | return true 472 | } 473 | 474 | // However, if a queue name has been specified as second argument, only that 475 | // particular queue should be returned. 476 | if len(args) > 1 { 477 | filter = func(queue Queue) bool { 478 | return queue.Name == args[1] 479 | } 480 | } 481 | 482 | queues, err := provider.GetQueues(filter) 483 | if err != nil { 484 | return err 485 | } 486 | 487 | table := tablewriter.NewWriter(os.Stdout) 488 | table.SetHeader([]string{"Name", "Durable", "Auto-Delete"}) 489 | 490 | for _, queue := range queues { 491 | row := make([]string, 3) 492 | row[0] = queue.Name 493 | row[1] = boolToString(queue.Durable) 494 | row[2] = boolToString(queue.AutoDelete) 495 | table.Append(row) 496 | } 497 | 498 | table.Render() 499 | 500 | return nil 501 | } 502 | 503 | // getBindingsCommand creates the `buneary get bindings` command, making sure that 504 | // exactly one argument is passed. 505 | func getBindingsCommand(options *globalOptions) *cobra.Command { 506 | getQueues := &cobra.Command{ 507 | Use: "bindings
", 508 | Short: "Get all available bindings", 509 | Args: cobra.ExactArgs(1), 510 | RunE: func(cmd *cobra.Command, args []string) error { 511 | return runGetBindings(options, args) 512 | }, 513 | } 514 | 515 | return getQueues 516 | } 517 | 518 | // getBindingCommand creates the `buneary get binding` command, making sure that exactly 519 | // three arguments are passed. 520 | func getBindingCommand(options *globalOptions) *cobra.Command { 521 | getQueue := &cobra.Command{ 522 | Use: "binding
", 523 | Short: "Get the binding or bindings between two resources", 524 | Args: cobra.ExactArgs(3), 525 | RunE: func(cmd *cobra.Command, args []string) error { 526 | return runGetBindings(options, args) 527 | }, 528 | } 529 | 530 | return getQueue 531 | } 532 | 533 | // runGetBindings either returns all bindings or - if a queue name has been specified as second 534 | // argument - a single binding. In case the password or both the user and password aren't 535 | // provided, it will go into interactive mode. 536 | // 537 | // This flexibility allows runGetBindings to be used by both `buneary get bindings` as well as 538 | // `buneary get binding`. 539 | func runGetBindings(options *globalOptions, args []string) error { 540 | var ( 541 | address = args[0] 542 | ) 543 | 544 | user, password := getOrReadInCredentials(options) 545 | 546 | provider := NewProvider(&RabbitMQConfig{ 547 | Address: address, 548 | User: user, 549 | Password: password, 550 | }) 551 | 552 | // The default filter will let pass all bindings regardless of their names. 553 | filter := func(_ Binding) bool { 554 | return true 555 | } 556 | 557 | // However, if a source exchange and a binding target have been specified as 558 | // second argument, only that particular binding should be returned. 559 | if len(args) > 2 { 560 | filter = func(binding Binding) bool { 561 | return binding.From.Name == args[1] && 562 | binding.TargetName == args[2] 563 | } 564 | } 565 | 566 | bindings, err := provider.GetBindings(filter) 567 | if err != nil { 568 | return err 569 | } 570 | 571 | table := tablewriter.NewWriter(os.Stdout) 572 | table.SetHeader([]string{"From", "Target", "Type", "Binding Key"}) 573 | 574 | for _, binding := range bindings { 575 | row := make([]string, 4) 576 | row[0] = binding.From.Name 577 | row[1] = binding.TargetName 578 | row[2] = string(binding.Type) 579 | row[3] = binding.Key 580 | table.Append(row) 581 | } 582 | 583 | table.Render() 584 | 585 | return nil 586 | } 587 | 588 | // getMessagesOptions defines options for reading messages. 589 | type getMessagesOptions struct { 590 | *globalOptions 591 | max int 592 | requeue bool 593 | force bool 594 | } 595 | 596 | // getMessagesCommand creates the `buneary get messages` command, making sure that exactly 597 | // two arguments are passed. 598 | func getMessagesCommand(options *globalOptions) *cobra.Command { 599 | getMessagesOptions := &getMessagesOptions{ 600 | globalOptions: options, 601 | } 602 | 603 | getMessages := &cobra.Command{ 604 | Use: "messages
", 605 | Short: "Get messages in a queue", 606 | Args: cobra.ExactArgs(2), 607 | RunE: func(cmd *cobra.Command, args []string) error { 608 | return runGetMessages(getMessagesOptions, args) 609 | }, 610 | } 611 | 612 | getMessages.Flags(). 613 | IntVar(&getMessagesOptions.max, "max", 1, "maximum messages to read") 614 | getMessages.Flags(). 615 | BoolVar(&getMessagesOptions.requeue, "requeue", false, "re-queue the messages after reading them") 616 | getMessages.Flags(). 617 | BoolVarP(&getMessagesOptions.force, "force", "f", false, "force running this command without opt-in") 618 | 619 | return getMessages 620 | } 621 | 622 | // runGetMessages gets messages by reading the command line data, setting the 623 | // configuration and calling the GetMessages function. In case the password or 624 | // both the user and password aren't provided, it will go into interactive mode. 625 | func runGetMessages(options *getMessagesOptions, args []string) error { 626 | var ( 627 | address = args[0] 628 | queue = args[1] 629 | ) 630 | 631 | message := "Reading the messages from the queue will de-queue them." + 632 | "To re-queue them, pass the --requeue flag. Do you want to continue?" 633 | 634 | if !options.force { 635 | ok := confirm(options.globalOptions, message) 636 | if !ok { 637 | return nil 638 | } 639 | } 640 | 641 | user, password := getOrReadInCredentials(options.globalOptions) 642 | 643 | provider := NewProvider(&RabbitMQConfig{ 644 | Address: address, 645 | User: user, 646 | Password: password, 647 | }) 648 | 649 | messages, err := provider.GetMessages(Queue{Name: queue}, options.max, options.requeue) 650 | if err != nil { 651 | return err 652 | } 653 | 654 | table := tablewriter.NewWriter(os.Stdout) 655 | table.SetHeader([]string{"Exchange", "Routing Key", "Body"}) 656 | 657 | for _, message := range messages { 658 | row := make([]string, 3) 659 | row[0] = message.Target.Name 660 | row[1] = message.RoutingKey 661 | row[2] = string(message.Body) 662 | table.Append(row) 663 | } 664 | 665 | table.Render() 666 | 667 | return nil 668 | } 669 | 670 | // publishOptions defines options for publishing a message. 671 | type publishOptions struct { 672 | *globalOptions 673 | headers string 674 | } 675 | 676 | // publishCommand creates the `buneary publish` command, making sure that exactly 677 | // four command arguments are passed. 678 | func publishCommand(options *globalOptions) *cobra.Command { 679 | publishOptions := &publishOptions{ 680 | globalOptions: options, 681 | } 682 | 683 | publish := &cobra.Command{ 684 | Use: "publish
", 685 | Short: "Publish a message to an exchange", 686 | Args: cobra.ExactArgs(4), 687 | RunE: func(cmd *cobra.Command, args []string) error { 688 | return runPublish(publishOptions, args) 689 | }, 690 | } 691 | 692 | publish.Flags(). 693 | StringVar(&publishOptions.headers, "headers", "", "headers as comma-separated key-value pairs") 694 | 695 | return publish 696 | } 697 | 698 | // runPublish publishes a message by reading the command line data, setting the 699 | // configuration and calling the PublishMessage function. In case the password or 700 | // both the user and password aren't provided, it will go into interactive mode. 701 | func runPublish(options *publishOptions, args []string) error { 702 | var ( 703 | address = args[0] 704 | exchange = args[1] 705 | routingKey = args[2] 706 | body = args[3] 707 | ) 708 | 709 | user, password := getOrReadInCredentials(options.globalOptions) 710 | 711 | provider := NewProvider(&RabbitMQConfig{ 712 | Address: address, 713 | User: user, 714 | Password: password, 715 | }) 716 | 717 | message := Message{ 718 | Target: Exchange{Name: exchange}, 719 | Headers: make(map[string]interface{}), 720 | RoutingKey: routingKey, 721 | Body: []byte(body), 722 | } 723 | 724 | if options.headers != "" { 725 | // Parse the message headers in the form key1=val1,key2=val2. If the headers 726 | // do not adhere to this syntax, an error is returned. In case the same key 727 | // exists multiple times, the last one wins. 728 | for _, header := range strings.Split(options.headers, ",") { 729 | tokens := strings.Split(strings.TrimSpace(header), "=") 730 | 731 | if len(tokens) != 2 { 732 | return errors.New("expected header in form key=value") 733 | } 734 | 735 | key := tokens[0] 736 | value := tokens[1] 737 | 738 | message.Headers[key] = value 739 | } 740 | } 741 | 742 | if err := provider.PublishMessage(message); err != nil { 743 | return err 744 | } 745 | 746 | _, _ = options.out.WriteString("message published successfully\n") 747 | 748 | return nil 749 | } 750 | 751 | // deleteCommand creates the `buneary delete` command without any functionality. 752 | func deleteCommand(options *globalOptions) *cobra.Command { 753 | delete := &cobra.Command{ 754 | Use: "delete ", 755 | Short: "Delete a resource", 756 | RunE: func(cmd *cobra.Command, args []string) error { 757 | return cmd.Help() 758 | }, 759 | } 760 | 761 | delete.AddCommand(deleteExchangeCommand(options)) 762 | delete.AddCommand(deleteQueueCommand(options)) 763 | 764 | return delete 765 | } 766 | 767 | // deleteExchangeCommand creates the `buneary delete exchange` command, making sure 768 | // that exactly two arguments are passed. 769 | func deleteExchangeCommand(options *globalOptions) *cobra.Command { 770 | deleteExchange := &cobra.Command{ 771 | Use: "exchange
", 772 | Short: "Delete an exchange", 773 | Args: cobra.ExactArgs(2), 774 | RunE: func(cmd *cobra.Command, args []string) error { 775 | return runDeleteExchange(options, args) 776 | }, 777 | } 778 | 779 | return deleteExchange 780 | } 781 | 782 | // runDeleteExchange deletes an exchange by reading the command line data, setting the 783 | // configuration and calling the DeleteExchange function. In case the password or 784 | // both the user and password aren't provided, it will go into interactive mode. 785 | func runDeleteExchange(options *globalOptions, args []string) error { 786 | var ( 787 | address = args[0] 788 | name = args[1] 789 | ) 790 | 791 | user, password := getOrReadInCredentials(options) 792 | 793 | provider := NewProvider(&RabbitMQConfig{ 794 | Address: address, 795 | User: user, 796 | Password: password, 797 | }) 798 | 799 | exchange := Exchange{ 800 | Name: name, 801 | } 802 | 803 | if err := provider.DeleteExchange(exchange); err != nil { 804 | return err 805 | } 806 | 807 | _, _ = options.out.WriteString("exchange deleted successfully\n") 808 | 809 | return nil 810 | } 811 | 812 | // deleteQueueCommand creates the `buneary delete queue` command, making sure 813 | // that exactly two arguments are passed. 814 | func deleteQueueCommand(options *globalOptions) *cobra.Command { 815 | deleteExchange := &cobra.Command{ 816 | Use: "queue
", 817 | Short: "Delete a queue", 818 | RunE: func(cmd *cobra.Command, args []string) error { 819 | return runDeleteQueue(options, args) 820 | }, 821 | } 822 | 823 | return deleteExchange 824 | } 825 | 826 | // runDeleteQueue deletes a queue by reading the command line data, setting the 827 | // configuration and calling the DeleteQueue function. In case the password or 828 | // both the user and password aren't provided, it will go into interactive mode. 829 | func runDeleteQueue(options *globalOptions, args []string) error { 830 | var ( 831 | address = args[0] 832 | name = args[1] 833 | ) 834 | 835 | user, password := getOrReadInCredentials(options) 836 | 837 | provider := NewProvider(&RabbitMQConfig{ 838 | Address: address, 839 | User: user, 840 | Password: password, 841 | }) 842 | 843 | queue := Queue{ 844 | Name: name, 845 | } 846 | 847 | if err := provider.DeleteQueue(queue); err != nil { 848 | return err 849 | } 850 | 851 | _, _ = options.out.WriteString("queue deleted successfully\n") 852 | 853 | return nil 854 | } 855 | 856 | // versionCommand creates the `buneary version` command for printing release 857 | // information. This data is injected by the CI pipeline. 858 | func versionCommand(options *globalOptions) *cobra.Command { 859 | version := &cobra.Command{ 860 | Use: "version", 861 | Short: "Print version information", 862 | RunE: func(cmd *cobra.Command, args []string) error { 863 | output := fmt.Sprintf("buneary version %s\n", version) 864 | _, _ = options.out.WriteString(output) 865 | 866 | return nil 867 | }, 868 | } 869 | 870 | return version 871 | } 872 | 873 | // getOrReadInCredentials either returns the credentials directly from the global 874 | // options or prompts the user to type them in. 875 | // 876 | // If both user and password have been set using the --user and --password flags, 877 | // those values will be used. Otherwise, the user will be asked to type in both. 878 | // 879 | // Another option might be to only ask the user for the password in case the --user 880 | // flag has been specified, but this is not implemented at the moment. 881 | func getOrReadInCredentials(options *globalOptions) (string, string) { 882 | user := options.user 883 | password := options.password 884 | 885 | if user != "" && password != "" { 886 | return user, password 887 | } 888 | 889 | reader := bufio.NewReader(os.Stdin) 890 | 891 | _, _ = options.out.WriteString("User: ") 892 | 893 | user, _ = reader.ReadString('\n') 894 | user = strings.TrimSpace(user) 895 | 896 | signalCh := make(chan os.Signal) 897 | signal.Notify(signalCh, os.Interrupt) 898 | 899 | go func() { 900 | <-signalCh 901 | os.Exit(0) 902 | }() 903 | 904 | _, _ = options.out.WriteString("Password: ") 905 | 906 | p, err := terminal.ReadPassword(int(syscall.Stdin)) 907 | if err != nil { 908 | _, _ = options.out.WriteString("\nerror reading password from stdin") 909 | os.Exit(1) 910 | } 911 | 912 | _, _ = options.out.WriteString("\n") 913 | 914 | password = string(p) 915 | 916 | return user, password 917 | } 918 | 919 | // confirm asks the user to confirm the given message or question by answering with 920 | // "y" for yes or "n" for no. Returns true if the user confirmed the message. 921 | func confirm(options *globalOptions, message string) bool { 922 | reader := bufio.NewReader(os.Stdin) 923 | output := fmt.Sprintf("%s [y/N] ", message) 924 | 925 | _, _ = options.out.WriteString(output) 926 | answer, _ := reader.ReadString('\n') 927 | answer = strings.TrimSpace(answer) 928 | 929 | _, _ = options.out.WriteString("\n") 930 | 931 | return answer == "y" || answer == "yes" 932 | } 933 | 934 | // boolToString returns "yes" if the given bool is true and "no" if it is false. 935 | func boolToString(source bool) string { 936 | if source { 937 | return "yes" 938 | } 939 | return "no" 940 | } 941 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dominikbraun/buneary 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/michaelklishin/rabbit-hole/v2 v2.6.0 7 | github.com/olekukonko/tablewriter v0.0.4 8 | github.com/spf13/cobra v1.1.1 9 | github.com/streadway/amqp v1.0.0 10 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 17 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 18 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 19 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 20 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 21 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 22 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 23 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 24 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 25 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 26 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 27 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 28 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 29 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 30 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 31 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 32 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 33 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 37 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 38 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 39 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 40 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 41 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 42 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 43 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 44 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 45 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 46 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 47 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 48 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 49 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 50 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 51 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 52 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 53 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 54 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 55 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 56 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 57 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 58 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 59 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 60 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 61 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 62 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 63 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 64 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 65 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 66 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 67 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 68 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 69 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 70 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 71 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 72 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 73 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 74 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 75 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 76 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 77 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 78 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 79 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 80 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 81 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 82 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 83 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 84 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 85 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 86 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 87 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 88 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 89 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 90 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 91 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 92 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 93 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 94 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 95 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 96 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 97 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 98 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 99 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 100 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 101 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 102 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 103 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 104 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 105 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 106 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 107 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 108 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 109 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 110 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 111 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 112 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 113 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 114 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 115 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 116 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 117 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 118 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 119 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 120 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 121 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 122 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 123 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 124 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 125 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 126 | github.com/michaelklishin/rabbit-hole/v2 v2.6.0 h1:oMLErqUVIYpXClYujgkCXtJNLswnth0LlJ8G3lKPF30= 127 | github.com/michaelklishin/rabbit-hole/v2 v2.6.0/go.mod h1:VZQTDutXFmoyrLvlRjM79MEPb0+xCLLhV5yBTjwMWkM= 128 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 129 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 130 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 131 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 132 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 133 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 134 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 135 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 136 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 137 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 138 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 139 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 140 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 141 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 142 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 143 | github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 144 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 145 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 146 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 147 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 148 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 149 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 150 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 151 | github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= 152 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 153 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 154 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 155 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 156 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 157 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 158 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 159 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 160 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 161 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 162 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 163 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 164 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 165 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 166 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 167 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 168 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 169 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 170 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 171 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 172 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 173 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 174 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 175 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 176 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 177 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 178 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 179 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 180 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 181 | github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= 182 | github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= 183 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 184 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 185 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 186 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 187 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 188 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 189 | github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= 190 | github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 191 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 192 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 193 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 194 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 195 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 196 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 197 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 198 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 199 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 200 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 201 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 202 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 203 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 204 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 205 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 206 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 207 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 208 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= 209 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 210 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 211 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 212 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 213 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 214 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 215 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 216 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 217 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 218 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 219 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 220 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 221 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 222 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 223 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 224 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 225 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 226 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 227 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 228 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 229 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 230 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 233 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 234 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 235 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 236 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 237 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 238 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 239 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 240 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 241 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 242 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 243 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 244 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 246 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= 247 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 248 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 249 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 250 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 251 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 252 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 253 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 254 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 255 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 256 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 257 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 258 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 259 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 260 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 261 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 262 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 263 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 264 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 265 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 266 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 267 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 268 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 269 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= 270 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 272 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 273 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 274 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 275 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 276 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 277 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 278 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 279 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 280 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 281 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 282 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 283 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 284 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 285 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 286 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 287 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 288 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 289 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 290 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 291 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 292 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 293 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 294 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 295 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 296 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 297 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 298 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 299 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 300 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 301 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 302 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 303 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 304 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 305 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 306 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 307 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 308 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 309 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 310 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 311 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 312 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 313 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 314 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 315 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 316 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 317 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 318 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 319 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 320 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 321 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 322 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 323 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 324 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 325 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 326 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 327 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 328 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 329 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 330 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 331 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 332 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 333 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 334 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 335 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 336 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 337 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 338 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 339 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 340 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 341 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 342 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 343 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 344 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 345 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 346 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 347 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 348 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 349 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 350 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 351 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 352 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 353 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 354 | -------------------------------------------------------------------------------- /jobs.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | CHANGELOG_FILE = "CHANGELOG.md" 5 | 6 | 7 | def main(): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("run", type=str, help="the job to run") 10 | parser.add_argument("--tag", type=str, help="the Git tag to work with") 11 | args = parser.parse_args() 12 | 13 | if args.run == "check-changelog": 14 | check_changelog(args.tag) 15 | elif args.run == "print-changelog": 16 | print_changelog(args.tag) 17 | 18 | sys.exit(0) 19 | 20 | 21 | def check_changelog(git_tag): 22 | """ 23 | Check if a new release tag is mentioned in the changelog. 24 | 25 | For a release tag like `v1.2.3`, the changelog has to contain a 26 | release section called `[1.2.3]`. If the release isn't mentioned 27 | in the changelog, exit with an error. 28 | """ 29 | # Cut off the `v` prefix to get the actual release number. 30 | search_expr = "[{0}]".format(git_tag[1:]) 31 | 32 | with open(CHANGELOG_FILE) as changelog: 33 | content = changelog.read() 34 | if search_expr not in content: 35 | msg = """You're trying to create a new release tag {0}, but that release is not mentioned 36 | in the changelog. Add a section called {1} to {2} and try again.""" \ 37 | .format(git_tag, search_expr, CHANGELOG_FILE) 38 | 39 | sys.exit(msg) 40 | 41 | 42 | def print_changelog(git_tag): 43 | """ 44 | Print the changelog for the given release tag by reading the 45 | changelog file. If the release tag does not exist as a release 46 | number in the changelog, the output will be empty. 47 | """ 48 | start = "## [{0}]".format(git_tag[1:]) 49 | # The ## [Unreleased] heading will be ignored. 50 | unreleased = "## [Unreleased]" 51 | end = "## [" 52 | 53 | capturing = False 54 | output = "" 55 | 56 | with open(CHANGELOG_FILE) as changelog: 57 | lines = changelog.readlines() 58 | for line in lines: 59 | # Start capturing if the line contains our release heading. 60 | if start in line and unreleased not in line: 61 | capturing = True 62 | continue 63 | # Stop capturing if we've reached the end, i.e. the next heading. 64 | if end in line and capturing: 65 | break 66 | if capturing: 67 | output += line 68 | 69 | print(output) 70 | 71 | 72 | main() 73 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikbraun/buneary/6a698ccac2b8dae5c0e895225696d8972334c938/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func main() { 8 | if err := rootCommand().Execute(); err != nil { 9 | log.Fatal(err) 10 | } 11 | } 12 | --------------------------------------------------------------------------------