├── .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 |
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 |
--------------------------------------------------------------------------------