├── .github
└── workflows
│ ├── release.yaml
│ └── test.yaml
├── .goreleaser.yml
├── LICENCE
├── Makefile
├── README.md
├── alias.go
├── alias_test.go
├── beta.go
├── beta_test.go
├── check.go
├── check_test.go
├── cmd
└── eskeeper
│ └── main.go
├── conf.go
├── conf_test.go
├── docker-compose.yaml
├── es.go
├── eskeeper.go
├── example
├── README.md
├── docker-compose.yaml
├── es.yaml
└── test.json
├── go.mod
├── go.sum
├── index.go
├── index_test.go
├── logo.png
├── main_test.go
├── reindex.go
├── reindex_test.go
└── testdata
├── es.reindex.yaml
├── es.yaml
├── invalid.json
├── invalidUpdateIndex.json
├── test.json
└── updateIndex.json
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - "v[0-9]+.[0-9]+.[0-9]+"
6 | jobs:
7 | goreleaser:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v1
12 | with:
13 | fetch-depth: 1
14 | - name: Setup Go
15 | uses: actions/setup-go@v2
16 | with:
17 | go-version: 1.17
18 | - name: Run GoReleaser
19 | uses: goreleaser/goreleaser-action@v2
20 | with:
21 | version: latest
22 | args: release --rm-dist
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Go Test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 |
11 | test:
12 | name: Test
13 | runs-on: ubuntu-latest
14 | steps:
15 |
16 | - name: Set up Go 1.15
17 | uses: actions/setup-go@v1
18 | with:
19 | go-version: 1.15
20 | id: go
21 |
22 | - name: Check out code into the Go module directory
23 | uses: actions/checkout@v2
24 |
25 | - name: check contents table
26 | run: |
27 | go test --race ./...
28 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | project_name: eskeeper
2 | env:
3 | - GO111MODULE=on
4 | before:
5 | hooks:
6 | - go mod tidy
7 | builds:
8 | - main: ./cmd/eskeeper
9 | binary: eskeeper
10 | ldflags:
11 | - -s -w
12 | - -X main.Version={{.Version}}
13 | - -X main.Revision={{.ShortCommit}}
14 | env:
15 | - CGO_ENABLED=0
16 | archives:
17 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
18 | replacements:
19 | darwin: darwin
20 | linux: linux
21 | windows: windows
22 | 386: i386
23 | amd64: x86_64
24 | format_overrides:
25 | - goos: windows
26 | format: zip
27 | release:
28 | prerelease: auto
29 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2020 Hirohito Sasakawa
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: setup
2 | setup: ## Setup Elasticsearch for dev
3 | docker-compose up -d
4 |
5 | .PHONY: help
6 | help: ## Display this help screen
7 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
eskeeper
4 | Tool managing Elasticsearch Index
5 |
6 |
7 |
[](https://godoc.org/github.com/po3rin/eskeeper) 
8 |
9 | eskeeper synchronizes index and alias with configuration files while ensuring idempotency. It still only supports WRITE. DELETE is not yet supported because the operation of deleting persistent data is dangerous and needs to be implemented carefully.
10 |
11 | :clipboard: [A Tour of eskeeper](https://github.com/po3rin/eskeeper/blob/main/example/README.md) explains more detail usage.
12 |
13 | ## :muscle: Currently supports
14 |
15 | ### mode
16 |
17 | - [x] CLI mode
18 | - [ ] Agent mode
19 |
20 | ### sync
21 |
22 | * index
23 | - [x] create
24 | - [x] status (open or close)
25 | - [x] reindex (only basic parameter)
26 | - [x] status(open/close only)
27 | - [ ] lifecycke
28 | - [ ] update mapping
29 | - [ ] delete
30 |
31 | * alias
32 | - [x] create
33 | - [x] update
34 | - [ ] delete
35 |
36 | ## :four_leaf_clover: How to use
37 |
38 | :clipboard: [A Tour of eskeeper](https://github.com/po3rin/eskeeper/blob/main/example/README.md) explains more detail usage.
39 |
40 | ### Quick Start
41 |
42 | eskeeper recieves yaml format data from stdin.
43 |
44 | ```bash
45 | $ go install github.com/po3rin/eskeeper/cmd/eskeeper
46 | $ eskeeper < es.yaml
47 | ```
48 |
49 | es.yaml is indices & aliases config file.
50 | Below is an example of es.yaml.
51 |
52 | ```yaml
53 | index:
54 | - name: test-v1 # index name
55 | mapping: testdata/test.json # index setting & mapping (json)
56 |
57 | - name: test-v2
58 | mapping: testdata/test.json
59 |
60 | - name: close-v1
61 | mapping: testdata/test.json
62 | status: close
63 |
64 | # reindex test-v1 -> reindex-v1
65 | - name: reindex-v1
66 | mapping: testdata/test.json
67 | reindex:
68 | source: test-v1
69 | slices: 3 # default=1
70 | waitForCompletion: true
71 |
72 | # 'on' field supports 2 hooks.
73 | # 'firstCreated': only when index is created for the first time.
74 | # 'always': always exec reindex.
75 | on: firstCreated
76 |
77 |
78 | alias:
79 | - name: alias1
80 | index:
81 | - test-v1
82 |
83 | # multi indicies
84 | - name: alias2
85 | index:
86 | - test-v1
87 | - test-v2
88 | ```
89 |
90 | results
91 |
92 | ```bach
93 | curl localhost:9200/_cat/indices
94 | yellow open test-v1 ... 1 1 0 0 208b 208b
95 | yellow open test-v2 ... 1 1 0 0 208b 208b
96 | yellow close close-v1 xxxxxxxxxxxx 1 1
97 | yellow open reindex-v1 ... 1 1 0 0 208b 208b
98 |
99 | curl localhost:9200/_cat/aliases
100 | alias2 test-v2 - - - -
101 | alias1 test-v1 - - - -
102 | alias2 test-v1 - - - -
103 | ```
104 |
105 |
106 | ### :triangular_ruler: CLI Options
107 |
108 | eskeeper supports flag & environment value.
109 |
110 | ```bash
111 | # use flags
112 | eskeeper -u user -p pass -e=http://localhost:9200,http://localhost9300 < testdata/es.yaml
113 |
114 | # use env
115 | ESKEEPER_ES_USER=user ESKEEPER_ES_PASS=pass ESKEEPER_ES_URLS=http://localhost:9200 eskeeper < testdata/es.yaml
116 | ```
117 |
118 | eskeeper can also execute validation only with validate subcommand.
119 |
120 | ```bash
121 | eskeeper validate < testdata/es.yaml
122 | ```
123 |
124 | pre-check stage is slow processing. you can skip pre-check stage using -s flag.
125 |
126 | ```bash
127 | eskeeper -s < testdata/es.yaml
128 | ```
129 |
130 | ## :mag_right: Internals
131 |
132 | eskeeper process is divided into four stages. verbose option lets you know eskeeper details.
133 |
134 | ```
135 | $ eskeeper < es.yaml
136 | loading config ...
137 |
138 | === validation stage ===
139 | [pass] index: test-v1
140 | [pass] index: test-v2
141 | [pass] index: close-v1
142 | [pass] alias: alias1
143 | [pass] alias: alias2
144 |
145 | === pre-check stage ===
146 | [pass] index: test-v1
147 | [pass] index: test-v2
148 | [pass] index: close-v1
149 | [pass] alias: alias1
150 | [pass] alias: alias2
151 |
152 | === sync stage ===
153 | [synced] index: test-v1
154 | [synced] index: test-v2
155 | [synced] index: close-v1
156 | [synced] alias: alias1
157 | [synced] alias: alias2
158 |
159 | === post-check stage ===
160 | [pass] index: test-v1
161 | [pass] index: test-v2
162 | [pass] index: close-v1
163 | [pass] alias: alias1
164 | [pass] alias: alias2
165 |
166 | succeeded
167 | ```
168 |
169 | #### validation stage
170 | * Validates config yaml format
171 |
172 | #### pre-check stage
173 |
174 | * Check if mapping file is valid format
175 | * Check if there is an index for alias
176 |
177 | #### sync stage
178 | * Sync indices and aliases with config
179 |
180 | The order of synchronization is as follows.
181 |
182 | ```
183 | create index
184 | ↓
185 | open index
186 | ↓
187 | update alias
188 | ↓
189 | close index
190 | ```
191 |
192 | Index close operation should be done after switching the alias.
193 | Because there can be downtime before switching aliases.
194 |
195 | #### post-check stage
196 | * Check if indices & aliases has been created
197 |
198 |
199 | ## :triangular_flag_on_post: Contributing
200 |
201 | Did you find something technically wrong, something to fix, or something? Please give me Issue or Pull Request !!
202 |
203 | ### Test
204 |
205 | eskeeper's test uses [github.com/ory/dockertest](github.com/ory/dockertest). So you need docker to test.
206 |
--------------------------------------------------------------------------------
/alias.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "io/ioutil"
9 | )
10 |
11 | func (c *esclient) existAlias(ctx context.Context, alias string) (bool, error) {
12 | exists := c.client.Indices.ExistsAlias
13 |
14 | res, err := exists(
15 | []string{alias},
16 | exists.WithContext(ctx),
17 | )
18 | if err != nil {
19 | return false, fmt.Errorf("check alias exists: %w", err)
20 | }
21 | if res.StatusCode == 200 {
22 | return true, nil
23 | }
24 | return false, nil
25 | }
26 | func (c *esclient) syncAlias(ctx context.Context, alias alias) error {
27 | i := c.client.Indices
28 |
29 | query := aliasQuery(alias.Name, alias.Indices)
30 |
31 | var buf bytes.Buffer
32 | if err := json.NewEncoder(&buf).Encode(query); err != nil {
33 | return fmt.Errorf("build query: %w", err)
34 | }
35 |
36 | res, err := i.UpdateAliases(&buf, i.UpdateAliases.WithContext(ctx))
37 | if err != nil {
38 | return fmt.Errorf("upsert aliases: %w", err)
39 | }
40 | if res.StatusCode != 200 {
41 | body, err := ioutil.ReadAll(res.Body)
42 | if err != nil {
43 | return fmt.Errorf("failed to sync alias [alias=%v, statusCode=%v]", alias.Name, res.StatusCode)
44 | }
45 | return fmt.Errorf("failed to sync alias [alias=%v, statusCode=%v, res=%v]", alias.Name, res.StatusCode, string(body))
46 | }
47 | return nil
48 | }
49 |
50 | func (c *esclient) syncAliases(ctx context.Context, conf config) error {
51 | for _, alias := range conf.Aliases {
52 | err := c.syncAlias(ctx, alias)
53 | if err != nil {
54 | c.logf("[fail] alias: %v\n", alias.Name)
55 | return fmt.Errorf("sync alias: %w", err)
56 | }
57 | c.logf("[synced] alias: %v\n", alias.Name)
58 | }
59 | return nil
60 | }
61 |
62 | func aliasQuery(aliasName string, indices []string) map[string]interface{} {
63 | Actions := make([]map[string]interface{}, 0, len(indices)+1)
64 |
65 | Actions = append(Actions, map[string]interface{}{
66 | "remove": map[string]interface{}{
67 | "index": "*",
68 | "alias": aliasName,
69 | },
70 | })
71 |
72 | for _, index := range indices {
73 | Actions = append(Actions, map[string]interface{}{
74 | "add": map[string]interface{}{
75 | "index": index,
76 | "alias": aliasName,
77 | },
78 | })
79 | }
80 |
81 | return map[string]interface{}{
82 | "actions": Actions,
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/alias_test.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestExistAlias(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | alias string
13 | setup func(tb testing.TB)
14 | want bool
15 | cleanup func(tb testing.TB)
16 | }{
17 | {
18 | name: "simple",
19 | alias: "alias-exist-v1",
20 | setup: func(tb testing.TB) {
21 | createTmpIndexHelper(tb, "alias-exist-index-v1")
22 | createTmpIndexHelper(tb, "alias-exist-index-v2")
23 | time.Sleep(10 * time.Second)
24 | createTmpAliasHelper(tb, "alias-exist-v1", "alias-exist-index-v1")
25 | createTmpAliasHelper(tb, "alias-exist-v1", "alias-exist-index-v2")
26 | },
27 | want: true,
28 | },
29 | {
30 | name: "not-found",
31 | alias: "not-found",
32 | setup: func(tb testing.TB) {
33 | },
34 | want: false,
35 | },
36 | }
37 |
38 | es, err := newEsClient([]string{url}, "", "")
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 |
43 | for _, tt := range tests {
44 | t.Run(tt.name, func(t *testing.T) {
45 | ctx := context.Background()
46 | tt.setup(t)
47 | ok, err := es.existAlias(ctx, tt.alias)
48 | if err != nil {
49 | t.Error(err)
50 | }
51 | if ok != tt.want {
52 | t.Errorf("want: %+v, got: %+v\n", tt.want, ok)
53 | }
54 | })
55 | }
56 | }
57 |
58 | func TestSyncAliases(t *testing.T) {
59 | tests := []struct {
60 | name string
61 | conf config
62 | setup func(tb testing.TB)
63 | cleanup func(tb testing.TB)
64 | }{
65 | {
66 | name: "simple",
67 | conf: config{
68 | Aliases: []alias{
69 | {
70 | Name: "test-sync-alias",
71 | Indices: []string{"test-v1", "test-v2"},
72 | },
73 | },
74 | },
75 | setup: func(tb testing.TB) {
76 | createTmpIndexHelper(tb, "test-v1")
77 | createTmpIndexHelper(tb, "test-v2")
78 | },
79 | },
80 | {
81 | name: "switch",
82 | conf: config{
83 | Aliases: []alias{
84 | {
85 | Name: "test-sync-alias",
86 | Indices: []string{"test-v3"},
87 | },
88 | },
89 | },
90 | setup: func(tb testing.TB) {
91 | createTmpIndexHelper(tb, "test-v3")
92 | },
93 | },
94 | }
95 |
96 | es, err := newEsClient([]string{url}, "", "")
97 | if err != nil {
98 | t.Fatal(err)
99 | }
100 |
101 | for _, tt := range tests {
102 | t.Run(tt.name, func(t *testing.T) {
103 | ctx := context.Background()
104 | tt.setup(t)
105 | err := es.syncAliases(ctx, tt.conf)
106 | if err != nil {
107 | t.Error(err)
108 | }
109 | })
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/beta.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "io/ioutil"
10 |
11 | "github.com/Cside/jsondiff"
12 | )
13 |
14 | type indexConfigWithName map[string]indexConfig
15 |
16 | type indexConfig struct {
17 | Settings map[string]interface{} `json:"settings"`
18 | Mappings map[string]interface{} `json:"mappings"`
19 | }
20 |
21 | func (c *esclient) index(ctx context.Context, index index) ([]byte, error) {
22 | get := c.client.Indices.Get
23 | res, err := get(
24 | []string{index.Name},
25 | get.WithContext(ctx),
26 | get.WithFlatSettings(true),
27 | )
28 | if err != nil {
29 | return nil, fmt.Errorf("get %v settings: %w", index.Name, err)
30 | }
31 | if res.StatusCode != 200 {
32 | body, err := ioutil.ReadAll(res.Body)
33 | if err != nil {
34 | return nil, fmt.Errorf("get %v settings: %w", index.Name, err)
35 | }
36 | return nil, fmt.Errorf("get %v settings: %v", index.Name, string(body))
37 | }
38 |
39 | b, err := ioutil.ReadAll(res.Body)
40 | if err != nil {
41 | return nil, fmt.Errorf("get %v settings: %w", index.Name, err)
42 | }
43 | return b, nil
44 | }
45 |
46 | // func (c *esclient) settings(ctx context.Context, index index) ([]byte, error) {
47 | // getSettings := c.client.Indices.GetSettings
48 | // res, err := getSettings(
49 | // getSettings.WithIndex(index.Name),
50 | // getSettings.WithContext(ctx),
51 | // )
52 | // if err != nil {
53 | // return nil, fmt.Errorf("get %v settings: %w", index.Name, err)
54 | // }
55 | // if res.StatusCode != 200 {
56 | // body, err := ioutil.ReadAll(res.Body)
57 | // if err != nil {
58 | // return nil, fmt.Errorf("get %v settings: %w", index.Name, err)
59 | // }
60 | // return nil, fmt.Errorf("get %v settings: %v", index.Name, string(body))
61 | // }
62 |
63 | // b, err := ioutil.ReadAll(res.Body)
64 | // if err != nil {
65 | // return nil, fmt.Errorf("get %v settings: %w", index.Name, err)
66 | // }
67 | // return b, nil
68 | // }
69 |
70 | // func (c *esclient) mapping(ctx context.Context, index index) ([]byte, error) {
71 | // getMapping := c.client.Indices.GetMapping
72 | // res, err := getMapping(
73 | // getMapping.WithIndex(index.Name),
74 | // getMapping.WithContext(ctx),
75 | // )
76 | // if err != nil {
77 | // return nil, fmt.Errorf("get %v mapping: %w", index.Name, err)
78 | // }
79 | // if res.StatusCode != 200 {
80 | // body, err := ioutil.ReadAll(res.Body)
81 | // if err != nil {
82 | // return nil, fmt.Errorf("get %v mapping: %w", index.Name, err)
83 | // }
84 | // return nil, fmt.Errorf("get %v mapping: %v", index.Name, string(body))
85 | // }
86 |
87 | // b, err := ioutil.ReadAll(res.Body)
88 | // if err != nil {
89 | // return nil, fmt.Errorf("get %v settings: %w", index.Name, err)
90 | // }
91 | // fmt.Println(string(b))
92 | // return b, nil
93 | // }
94 |
95 | // func (c *esclient) equalSettings(ctx context.Context, index index, settingJSON []byte) (bool, error) {
96 | // m, err := c.settings(ctx, index)
97 | // if err != nil {
98 | // return false, fmt.Errorf("get setting: %w", err)
99 | // }
100 |
101 | // if diff := jsondiff.Diff(m, settingJSON); diff != "" {
102 | // // fmt.Printf("detect settings diff:\n%s", diff)
103 | // return false, nil
104 | // }
105 | // return true, nil
106 | // }
107 |
108 | func (c *esclient) equalMappingsProperties(ctx context.Context, index index, mappingsJSON []byte) (bool, error) {
109 | res, err := c.index(ctx, index)
110 | if err != nil {
111 | return false, fmt.Errorf("get mappings: %w", err)
112 | }
113 |
114 | gotConf := make(indexConfigWithName, 0)
115 |
116 | err = json.Unmarshal(res, &gotConf)
117 | if err != nil {
118 | return false, fmt.Errorf("unmarshal mapping json: %w", err)
119 | }
120 |
121 | v, ok := gotConf[index.Name]
122 | if !ok {
123 | return false, errors.New("get index response dose not contain index name field")
124 | }
125 |
126 | b, err := json.Marshal(v.Mappings)
127 | if err != nil {
128 | return false, fmt.Errorf("marshal properties json: %w", err)
129 | }
130 |
131 | if diff := jsondiff.Diff(b, mappingsJSON); diff != "" {
132 | // fmt.Printf("detect mapping diff:\n%s", diff)
133 | return false, nil
134 |
135 | }
136 | return true, nil
137 | }
138 |
139 | func (c *esclient) updateIndex(ctx context.Context, index index) error {
140 | putMapping := c.client.Indices.PutMapping
141 | // putSetting := c.client.Indices.PutSettings
142 |
143 | b, err := ioutil.ReadFile(index.Mapping)
144 | if err != nil {
145 | return fmt.Errorf("open mapping file: %w", err)
146 | }
147 |
148 | config := &indexConfig{}
149 | err = json.Unmarshal(b, config)
150 | if err != nil {
151 | return fmt.Errorf("unmarshal mapping json: %w", err)
152 | }
153 |
154 | // mapping --------
155 | if config.Mappings == nil {
156 | // dose not contain
157 | // return fmt.Errorf("get mappings from file: %w", err)
158 | } else {
159 | j, err := json.Marshal(config.Mappings)
160 | if err != nil {
161 | return fmt.Errorf("marshal mappings json: %w", err)
162 | }
163 |
164 | ok, err := c.equalMappingsProperties(ctx, index, j)
165 | if err != nil {
166 | return err
167 | }
168 |
169 | if !ok {
170 | res, err := putMapping(
171 | bytes.NewReader(j),
172 | putMapping.WithIndex(index.Name),
173 | putMapping.WithContext(ctx),
174 | )
175 | if err != nil {
176 | return fmt.Errorf("get %v mapping: %w", index, err)
177 | }
178 | if res.StatusCode != 200 {
179 | body, err := ioutil.ReadAll(res.Body)
180 | if err != nil {
181 | return fmt.Errorf("update %v mapping: %w", index.Name, err)
182 | }
183 | return fmt.Errorf("update %v mapping: %v", index.Name, string(body))
184 | }
185 | }
186 | }
187 |
188 | // // setting -------
189 | // if config.Settings == nil {
190 | // // dose not contain
191 | // // return fmt.Errorf("get settings from file: %w", err)
192 | // return nil
193 | // }
194 |
195 | // j, err := json.Marshal(config.Settings)
196 | // if err != nil {
197 | // return fmt.Errorf("marshal mappings json: %w", err)
198 | // }
199 |
200 | // ok, err := c.equalSettings(ctx, index, j)
201 | // if err != nil {
202 | // return err
203 | // }
204 | // if ok {
205 | // return nil
206 | // }
207 |
208 | // res, err := putSetting(
209 | // bytes.NewReader(j),
210 | // putSetting.WithIndex(index.Name),
211 | // putSetting.WithContext(ctx),
212 | // )
213 | // if err != nil {
214 | // return fmt.Errorf("update %v setting: %w", index, err)
215 | // }
216 | // if res.StatusCode != 200 {
217 | // body, err := ioutil.ReadAll(res.Body)
218 | // if err != nil {
219 | // return fmt.Errorf("update %v setting: %w", index.Name, err)
220 | // }
221 | // return fmt.Errorf("update %v setting: %v", index.Name, string(body))
222 | // }
223 |
224 | return nil
225 | }
226 |
--------------------------------------------------------------------------------
/beta_test.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | func TestUpdateIndex(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | index index
12 | setup func(tb testing.TB)
13 | wantErr bool
14 | }{
15 | {
16 | name: "simple",
17 | index: index{
18 | Name: "update-test-v1",
19 | Mapping: "testdata/updateIndex.json",
20 | },
21 | setup: func(tb testing.TB) {
22 | createTmpIndexHelper(tb, "update-test-v1")
23 | },
24 | wantErr: false,
25 | },
26 | // {
27 | // name: "same",
28 | // index: index{
29 | // Name: "update-test-v2",
30 | // Mapping: "testdata/test.json",
31 | // },
32 | // setup: func(tb testing.TB) {
33 | // createTmpIndexHelper(tb, "update-test-v2")
34 | // },
35 | // wantErr: false,
36 | // },
37 | // {
38 | // name: "invalid-update",
39 | // index: index{
40 | // Name: "update-test-v3",
41 | // Mapping: "testdata/invalidUpdateIndex.json",
42 | // },
43 | // setup: func(tb testing.TB) {
44 | // createTmpIndexHelper(tb, "update-test-v3")
45 | // },
46 | // wantErr: true,
47 | // },
48 | }
49 |
50 | es, err := newEsClient([]string{url}, "", "")
51 | if err != nil {
52 | t.Fatal(err)
53 | }
54 |
55 | for _, tt := range tests {
56 | t.Run(tt.name, func(t *testing.T) {
57 | ctx := context.Background()
58 | tt.setup(t)
59 | err := es.updateIndex(ctx, tt.index)
60 | if tt.wantErr && err == nil {
61 | t.Error("expect error")
62 | }
63 | if !tt.wantErr && err != nil {
64 | t.Error(err)
65 | }
66 | })
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/check.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/gofrs/uuid"
8 | )
9 |
10 | func (c *esclient) preCheckIndex(ctx context.Context, ix index) error {
11 | // generate uuid for pre-check create index
12 | u2, err := uuid.NewV4()
13 | if err != nil {
14 | return fmt.Errorf("generate UUID for pre-check: %w", err)
15 | }
16 |
17 | preIndex := index{
18 | Name: fmt.Sprintf("eskeeper-%s", u2.String()),
19 | Mapping: ix.Mapping,
20 | }
21 |
22 | err = c.syncIndex(ctx, preIndex)
23 | if err != nil {
24 | return fmt.Errorf("pre-check: pre create using random name index: %w", err)
25 | }
26 |
27 | err = c.deleteIndex(ctx, preIndex.Name)
28 | if err != nil {
29 | return fmt.Errorf("pre-check: delete pre-created index: %w", err)
30 | }
31 | return nil
32 | }
33 |
34 | func (c *esclient) preCheckAlias(ctx context.Context, alias alias, createIndices map[string]struct{}) error {
35 | // TODO: check duplicated name
36 | // ok, err := c.existIndex(ctx, alias.Name)
37 | // if err != nil {
38 | // return fmt.Errorf("pre-check: checks for duplicate index and alias names: %w", err)
39 | // }
40 | // if ok {
41 | // return fmt.Errorf("pre-check: detects duplicate index and alias names %v", alias.Name)
42 | // }
43 |
44 | for _, index := range alias.Indices {
45 | if _, ok := createIndices[index]; ok {
46 | continue
47 | }
48 | ok, err := c.existIndex(ctx, index)
49 | if err != nil {
50 | return fmt.Errorf("pre-check: check index %v exists for alias %v: %w", index, alias.Name, err)
51 | }
52 | if !ok {
53 | c.logf("[fail] alias: %v\n", alias.Name)
54 | return fmt.Errorf("pre-check: index %v for alias %v is not found", index, alias.Name)
55 | }
56 | }
57 |
58 | return nil
59 | }
60 |
61 | func (c *esclient) preCheck(ctx context.Context, conf config) error {
62 | // use alias pre-check
63 | createIndices := make(map[string]struct{}, 0)
64 |
65 | for _, ix := range conf.Indices {
66 | ok, err := c.existIndex(ctx, ix.Name)
67 | if err != nil {
68 | return fmt.Errorf("pre-check: check index %v exists: %w", ix.Name, err)
69 | }
70 | if ok {
71 | createIndices[ix.Name] = struct{}{}
72 | c.logf("[skip] index %v already exists\n", ix.Name)
73 | continue
74 | }
75 |
76 | createIndices[ix.Name] = struct{}{}
77 | err = c.preCheckIndex(ctx, ix)
78 | if err != nil {
79 | c.logf("[fail] index: %v\n", ix.Name)
80 | return err
81 | }
82 | c.logf("[pass] index: %v\n", ix.Name)
83 | }
84 |
85 | // check target index exists
86 | for _, alias := range conf.Aliases {
87 | err := c.preCheckAlias(ctx, alias, createIndices)
88 | if err != nil {
89 | c.logf("[fail] alias: %v\n", alias.Name)
90 | return err
91 | }
92 | c.logf("[pass] alias: %v\n", alias.Name)
93 | }
94 |
95 | return nil
96 | }
97 |
98 | // postCheck checks created index & alias by name only.
99 | func (c *esclient) postCheck(ctx context.Context, conf config) error {
100 | for _, index := range conf.Indices {
101 | ok, err := c.existIndex(ctx, index.Name)
102 | if err != nil {
103 | c.logf("[fail] index: %v\n", index.Name)
104 | return fmt.Errorf("post-check: check created index %v exist: %w", index.Name, err)
105 | }
106 | if !ok {
107 | c.logf("[fail] index: %v\n", index.Name)
108 | return fmt.Errorf("post-check: created index %v is not found", index.Name)
109 | }
110 | c.logf("[pass] index: %v\n", index.Name)
111 | }
112 |
113 | for _, alias := range conf.Aliases {
114 | ok, err := c.existAlias(ctx, alias.Name)
115 | if err != nil {
116 | c.logf("[fail] alias: %v\n", alias.Name)
117 | return fmt.Errorf("post-check: check created alias %v exist: %w", alias.Name, err)
118 | }
119 | if !ok {
120 | c.logf("[fail] alias: %v\n", alias.Name)
121 | return fmt.Errorf("post-check: created alias %v is not found", alias.Name)
122 | }
123 | c.logf("[pass] alias: %v\n", alias.Name)
124 | }
125 | return nil
126 | }
127 |
--------------------------------------------------------------------------------
/check_test.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | func TestPreCheck(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | conf config
12 | wantErr bool
13 | }{
14 | {
15 | name: "simple",
16 | conf: config{
17 | Indices: []index{
18 | {
19 | Name: "precheck1",
20 | Mapping: "testdata/test.json",
21 | },
22 | },
23 | },
24 | wantErr: false,
25 | },
26 | {
27 | name: "invalid-field",
28 | conf: config{
29 | Indices: []index{
30 | {
31 | Name: "precheck2",
32 | Mapping: "testdata/invalid.json",
33 | },
34 | },
35 | },
36 | wantErr: true,
37 | },
38 | {
39 | name: "not-found-index",
40 | conf: config{
41 | Indices: []index{
42 | {
43 | Name: "precheck3",
44 | Mapping: "testdata/invalid.json",
45 | },
46 | },
47 | Aliases: []alias{
48 | {
49 | Name: "precheck-alias",
50 | Indices: []string{
51 | "precheck3",
52 | "precheck4", // not found
53 | },
54 | },
55 | },
56 | },
57 | wantErr: true,
58 | },
59 | // {
60 | // name: "duplicated name",
61 | // conf: config{
62 | // Indices: []index{
63 | // {
64 | // Name: "duplicated-name",
65 | // Mapping: "testdata/test.json",
66 | // },
67 | // },
68 | // Aliases: []alias{
69 | // {
70 | // Name: "duplicated-name",
71 | // Indices: []string{
72 | // "duplicated-name",
73 | // },
74 | // },
75 | // },
76 | // },
77 | // wantErr: true,
78 | // },
79 | }
80 |
81 | es, err := newEsClient([]string{url}, "", "")
82 | if err != nil {
83 | t.Fatal(err)
84 | }
85 |
86 | for _, tt := range tests {
87 | t.Run(tt.name, func(t *testing.T) {
88 | ctx := context.Background()
89 | err := es.preCheck(ctx, tt.conf)
90 | if tt.wantErr && err == nil {
91 | t.Error("expect error")
92 | }
93 | if !tt.wantErr && err != nil {
94 | t.Error(err)
95 | }
96 | })
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/cmd/eskeeper/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/po3rin/eskeeper"
9 | "github.com/spf13/cobra"
10 | "github.com/spf13/pflag"
11 | "github.com/spf13/viper"
12 | "golang.org/x/crypto/ssh/terminal"
13 | )
14 |
15 | var rootCmd = &cobra.Command{
16 | Use: "eskeeper",
17 | Short: "eskeeper synchronizes index and alias with configuration files while ensuring idempotency.",
18 | Run: func(cmd *cobra.Command, args []string) {
19 | k, err := eskeeper.New(
20 | viper.GetStringSlice("es_urls"),
21 | eskeeper.UserName(viper.GetString("es_user")),
22 | eskeeper.Pass(viper.GetString("es_pass")),
23 | eskeeper.Verbose(viper.GetBool("verbose")),
24 | eskeeper.SkipPreCheck(viper.GetBool("skip_precheck")),
25 | )
26 | if err != nil {
27 | fmt.Fprintln(os.Stdout, err)
28 | os.Exit(1)
29 | }
30 |
31 | if terminal.IsTerminal(int(os.Stdin.Fd())) {
32 | fmt.Fprintln(os.Stdout, "Currently does not support interactive mode")
33 | os.Exit(1)
34 | }
35 |
36 | ctx := context.Background()
37 | err = k.Sync(ctx, os.Stdin)
38 | if err != nil {
39 | fmt.Fprintln(os.Stdout, err)
40 | os.Exit(1)
41 | }
42 | },
43 | }
44 |
45 | var validate = &cobra.Command{
46 | Use: "validate",
47 | Short: "Validates config",
48 | Run: func(cmd *cobra.Command, args []string) {
49 | k, err := eskeeper.New(
50 | []string{},
51 | eskeeper.Verbose(true),
52 | )
53 | if err != nil {
54 | fmt.Fprintln(os.Stdout, err)
55 | os.Exit(1)
56 | }
57 |
58 | if terminal.IsTerminal(int(os.Stdin.Fd())) {
59 | fmt.Fprintln(os.Stdout, "Currently does not support interactive mode")
60 | os.Exit(1)
61 | }
62 |
63 | ctx := context.Background()
64 | err = k.Validate(ctx, os.Stdin)
65 | if err != nil {
66 | fmt.Fprintln(os.Stdout, err)
67 | os.Exit(1)
68 | }
69 | fmt.Println("pass")
70 | },
71 | }
72 |
73 | func init() {
74 | rootCmd.AddCommand(validate)
75 | viper.SetEnvPrefix("eskeeper")
76 | viper.AutomaticEnv()
77 |
78 | pflag.StringP("es_user", "u", "", "Elasticsearch user name")
79 | pflag.StringP("es_pass", "p", "", "Elasticsearch password")
80 | pflag.StringSliceP("es_urls", "e", []string{"http://localhost:9200"}, "Elasticserch endpoint URLs (comma delimited)")
81 | pflag.BoolP("verbose", "v", false, "Make the operation more talkative")
82 | pflag.BoolP("skip_precheck", "s", false, "Skip pre-check stage")
83 |
84 | viper.BindPFlags(pflag.CommandLine)
85 | }
86 |
87 | func main() {
88 | if err := rootCmd.Execute(); err != nil {
89 | fmt.Fprintln(os.Stdout, err)
90 | os.Exit(1)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/conf.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 |
10 | "github.com/goccy/go-yaml"
11 | )
12 |
13 | var status = map[string]struct{}{
14 | "open": struct{}{},
15 | "close": struct{}{},
16 | "": struct{}{}, // default
17 | }
18 |
19 | var reindexOn = map[string]struct{}{
20 | "always": struct{}{},
21 | "firstCreated": struct{}{},
22 | "": struct{}{}, // default
23 | }
24 |
25 | type config struct {
26 | Indices []index `json:"index"`
27 | Aliases []alias `json:"alias"` // supports close only
28 | }
29 |
30 | type index struct {
31 | Name string `json:"name"`
32 | Mapping string `json:"mapping"`
33 | Status string `json:"status"`
34 | Reindex reindex `json:"reindex"`
35 | }
36 |
37 | type reindex struct {
38 | Source string `json:"source"`
39 | Slices int `json:"slices"`
40 | WaitForCompletion bool `json:"waitForCompletion"`
41 | On string `json:"on"`
42 | }
43 |
44 | type alias struct {
45 | Name string `json:"name"`
46 | Indices []string `json:"index"`
47 | }
48 |
49 | func yaml2Conf(reader io.Reader) (config, error) {
50 | conf := config{}
51 |
52 | b, err := ioutil.ReadAll(reader)
53 | if err != nil {
54 | return conf, err
55 | }
56 |
57 | if err := yaml.Unmarshal(b, &conf); err != nil {
58 | return conf, err
59 | }
60 |
61 | return conf, nil
62 | }
63 |
64 | func validateIndex(index index) error {
65 | if index.Name == "" {
66 | return errors.New("index name is empty")
67 | }
68 | if index.Mapping != "" {
69 | m, err := ioutil.ReadFile(index.Mapping)
70 | if err != nil {
71 | return fmt.Errorf("read file %v: %w", index.Mapping, err)
72 | }
73 | // validate json format
74 | var jsonStr map[string]interface{}
75 | if err := json.Unmarshal(m, &jsonStr); err != nil {
76 | return fmt.Errorf("mapping json is invalid: %w", err)
77 | }
78 | }
79 | _, ok := status[index.Status]
80 | if !ok {
81 | return fmt.Errorf("unsupported status %v", index.Status)
82 | }
83 |
84 | if index.Reindex.Source != "" {
85 | if index.Status == "close" {
86 | return errors.New("unsupported close status and reindex cannot be used together")
87 | }
88 | _, ok := reindexOn[index.Reindex.On]
89 | if !ok {
90 | return fmt.Errorf("unsupported reindex hook %v. [always or firstCreated]", index.Reindex.On)
91 | }
92 | }
93 |
94 | return nil
95 | }
96 |
97 | func validateAlias(alias alias) error {
98 | if alias.Name == "" {
99 | return errors.New("alias name is empty")
100 | }
101 |
102 | if len(alias.Indices) == 0 {
103 | return fmt.Errorf("no indices in %v alias", alias.Name)
104 | }
105 |
106 | for _, index := range alias.Indices {
107 | if index == "" {
108 | return errors.New("index name is empty")
109 | }
110 | }
111 | return nil
112 | }
113 |
114 | func (e *Eskeeper) validateConfigFormat(c config) error {
115 | createIndices := make(map[string]struct{}, 0)
116 |
117 | for _, index := range c.Indices {
118 | _, exist := createIndices[index.Name]
119 | if exist {
120 | e.logf("[fail] index: %v\n", index.Name)
121 | return fmt.Errorf("duplicated index name %v", index.Name)
122 | }
123 |
124 | createIndices[index.Name] = struct{}{}
125 |
126 | err := validateIndex(index)
127 | if err != nil {
128 | e.logf("[fail] index: %v\n", index.Name)
129 | return fmt.Errorf("validate index: %w", err)
130 | }
131 |
132 | e.logf("[pass] index: %v\n", index.Name)
133 | }
134 |
135 | for _, alias := range c.Aliases {
136 | _, ok := createIndices[alias.Name]
137 | if ok {
138 | e.logf("[fail] alias: %v\n", alias.Name)
139 | return fmt.Errorf("alias name %v is a duplicate of an index name that already exists", alias.Name)
140 | }
141 |
142 | err := validateAlias(alias)
143 | if err != nil {
144 | e.logf("[fail] alias: %v\n", alias.Name)
145 | return fmt.Errorf("validate alias: %w", err)
146 | }
147 |
148 | e.logf("[pass] alias: %v\n", alias.Name)
149 | }
150 | return nil
151 | }
152 |
--------------------------------------------------------------------------------
/conf_test.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "os"
5 | "reflect"
6 |
7 | "testing"
8 | )
9 |
10 | func TestYaml2Conf(t *testing.T) {
11 | tests := []struct {
12 | name string
13 | yaml string
14 | want config
15 | }{
16 | {
17 | name: "simple",
18 | yaml: "testdata/es.yaml",
19 | want: config{
20 | Indices: []index{
21 | {
22 | Name: "test-v1",
23 | Mapping: "testdata/test.json",
24 | },
25 | {
26 | Name: "test-v2",
27 | Mapping: "testdata/test.json",
28 | },
29 | {
30 | Name: "close-v1",
31 | Mapping: "testdata/test.json",
32 | Status: "close",
33 | },
34 | },
35 | Aliases: []alias{
36 | {
37 | Name: "alias1",
38 | Indices: []string{"test-v1"},
39 | },
40 | {
41 | Name: "alias2",
42 | Indices: []string{"test-v1", "test-v2"},
43 | },
44 | },
45 | },
46 | },
47 | {
48 | name: "reindex",
49 | yaml: "testdata/es.reindex.yaml",
50 | want: config{
51 | Indices: []index{
52 | {
53 | Name: "test-v1",
54 | Mapping: "testdata/test.json",
55 | },
56 | {
57 | Name: "reindex-v1",
58 | Mapping: "testdata/test.json",
59 | Reindex: reindex{
60 | Source: "test-v1",
61 | Slices: 3,
62 | WaitForCompletion: true,
63 | On: "firstCreated",
64 | },
65 | },
66 | },
67 | Aliases: []alias{
68 | {
69 | Name: "alias1",
70 | Indices: []string{"test-v2"},
71 | },
72 | },
73 | },
74 | },
75 | }
76 |
77 | for _, tt := range tests {
78 | t.Run(tt.name, func(t *testing.T) {
79 | r, err := os.Open(tt.yaml)
80 | if err != nil {
81 | t.Fatal(err)
82 | }
83 | got, err := yaml2Conf(r)
84 | if err != nil {
85 | t.Fatal(err)
86 | }
87 | if !reflect.DeepEqual(got, tt.want) {
88 | t.Errorf("\nwant: %+v\ngot : %+v\n", tt.want, got)
89 | }
90 | })
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | elasticsearch:
4 | image: docker.elastic.co/elasticsearch/elasticsearch:7.15.1
5 | ports:
6 | - "9200:9200"
7 | - "9300:9300"
8 | environment:
9 | - discovery.type=single-node
10 |
--------------------------------------------------------------------------------
/es.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/cenkalti/backoff/v4"
8 | "github.com/elastic/go-elasticsearch/v7"
9 | )
10 |
11 | type esclient struct {
12 | client *elasticsearch.Client
13 | verbose bool
14 | }
15 |
16 | func newEsClient(urls []string, user, pass string) (*esclient, error) {
17 | retryBackoff := backoff.NewExponentialBackOff()
18 | retryBackoff.InitialInterval = time.Second
19 |
20 | conf := elasticsearch.Config{
21 | Addresses: urls,
22 | Username: user,
23 | Password: pass,
24 | RetryOnStatus: []int{408, 429, 502, 503, 504},
25 | RetryBackoff: func(i int) time.Duration {
26 | if i == 1 {
27 | retryBackoff.Reset()
28 | }
29 | d := retryBackoff.NextBackOff()
30 | return d
31 | },
32 | }
33 | es, err := elasticsearch.NewClient(conf)
34 | if err != nil {
35 | return nil, err
36 | }
37 | return &esclient{
38 | client: es,
39 | }, nil
40 | }
41 |
42 | func (e *esclient) log(msg string) {
43 | if e.verbose {
44 | fmt.Println(msg)
45 | }
46 | }
47 |
48 | func (e *esclient) logf(format string, a ...interface{}) {
49 | if e.verbose {
50 | fmt.Printf(format, a...)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/eskeeper.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | // Eskeeper manages indices & aliases.
10 | type Eskeeper struct {
11 | client *esclient
12 |
13 | // options
14 | user string
15 | pass string
16 | verbose bool
17 | skipPreCheck bool
18 | }
19 |
20 | // NewOption is optional func for eskeeper.New
21 | type NewOption func(*Eskeeper)
22 |
23 | // UserName is optional func for Elasticsearch user name.
24 | func UserName(name string) NewOption {
25 | return func(e *Eskeeper) {
26 | e.user = name
27 | }
28 | }
29 |
30 | // Pass is optional func for Elasticsearch password.
31 | func Pass(pass string) NewOption {
32 | return func(e *Eskeeper) {
33 | e.pass = pass
34 | }
35 | }
36 |
37 | // Verbose is optional func for verbose option.
38 | func Verbose(v bool) NewOption {
39 | return func(e *Eskeeper) {
40 | e.verbose = v
41 | }
42 | }
43 |
44 | // SkipPreCheck is optional func for skipping pre-check-stage..
45 | func SkipPreCheck(v bool) NewOption {
46 | return func(e *Eskeeper) {
47 | e.skipPreCheck = v
48 | }
49 | }
50 |
51 | // New inits Eskeeper.
52 | func New(urls []string, opts ...NewOption) (*Eskeeper, error) {
53 | eskeeper := &Eskeeper{}
54 |
55 | for _, opt := range opts {
56 | opt(eskeeper)
57 | }
58 |
59 | es, err := newEsClient(urls, eskeeper.user, eskeeper.pass)
60 | if err != nil {
61 | return nil, err
62 | }
63 |
64 | es.verbose = eskeeper.verbose
65 | eskeeper.client = es
66 |
67 | return eskeeper, nil
68 | }
69 |
70 | // Sync synchronizes config & Elasticsearch State.
71 | func (e *Eskeeper) Sync(ctx context.Context, reader io.Reader) error {
72 | e.log("loading config ...")
73 | conf, err := yaml2Conf(reader)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | e.log("\n=== validation stage ===")
79 | err = e.validateConfigFormat(conf)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | if !e.skipPreCheck {
85 | e.log("\n=== pre-check stage ===")
86 | err = e.client.preCheck(ctx, conf)
87 | if err != nil {
88 | return err
89 | }
90 | }
91 |
92 | e.log("\n=== sync stage ===")
93 | err = e.client.syncIndices(ctx, conf)
94 | if err != nil {
95 | return err
96 | }
97 |
98 | err = e.client.syncAliases(ctx, conf)
99 | if err != nil {
100 | return err
101 | }
102 |
103 | err = e.client.syncCloseStatus(ctx, conf)
104 | if err != nil {
105 | return err
106 | }
107 |
108 | e.log("\n=== post-check stage ===")
109 | err = e.client.postCheck(ctx, conf)
110 | if err != nil {
111 | return err
112 | }
113 |
114 | e.log("\nsucceeded")
115 | return nil
116 | }
117 |
118 | // Validate validates cofig.
119 | func (e *Eskeeper) Validate(ctx context.Context, reader io.Reader) error {
120 | conf, err := yaml2Conf(reader)
121 | if err != nil {
122 | return err
123 | }
124 | err = e.validateConfigFormat(conf)
125 | if err != nil {
126 | return err
127 | }
128 | return nil
129 | }
130 |
131 | func (e *Eskeeper) log(msg string) {
132 | if e.verbose {
133 | fmt.Printf("\x1b[34m%s\x1b[0m\n", msg)
134 | }
135 | }
136 |
137 | func (e *Eskeeper) logf(format string, a ...interface{}) {
138 | if e.verbose {
139 | fmt.Printf(format, a...)
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # A Tour of eskeeper
2 |
3 | A Tour of eskeeper explains the basic usage of eskeeper.
4 |
5 | The files to be used are prepared under the `example` directory.
6 |
7 | ```tree
8 | example
9 | ├── README.md
10 | ├── docker-compose.yaml
11 | ├── es.yaml
12 | ├── README.md
13 | └── test.json
14 | ```
15 |
16 | ## start Elasticsearch
17 |
18 | First, start Elasticserarch.
19 |
20 | ```bash
21 | $ docker-compose up -d --build
22 | ```
23 |
24 | ## create indices & aliases
25 |
26 | Next, prepare index setting JSON file `test.json`. The content of this file is the same as the request body of the [Create index API](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html)
27 |
28 | ```json
29 | {
30 | "settings": {
31 | "number_of_shards": 1,
32 | "number_of_replicas": 1,
33 | "analysis": {
34 | "analyzer": {
35 | "test_analyzer": {
36 | "type": "custom",
37 | "tokenizer": "standard",
38 | "filter": ["lowercase"]
39 | }
40 | }
41 | }
42 | },
43 | "mappings": {
44 | "properties": {
45 | "id": {
46 | "type": "long"
47 | },
48 | "title": {
49 | "type": "text"
50 | },
51 | "body": {
52 | "type": "text",
53 | "analyzer": "test_analyzer"
54 | }
55 | }
56 | }
57 | }
58 | ```
59 |
60 | create `es.yaml` to manage indices.
61 |
62 | ```yaml
63 | index:
64 | - name: test-v1 # index name
65 | mapping: test.json # index setting & mapping (json)
66 |
67 | - name: test-v2
68 | mapping: test.json
69 |
70 | - name: close-v1
71 | mapping: test.json
72 | status: close
73 |
74 | alias:
75 | - name: alias1
76 | index:
77 | - test-v1
78 |
79 | # multi indicies
80 | - name: alias2
81 | index:
82 | - test-v1
83 | - test-v2
84 | ```
85 |
86 | You can see in this file that you create 3 indices and 2 aliases. You can create an index with the same settings as the test.json created earlier. `close-v1` index use `status: close`. This allows closed indices to not have to maintain internal data structures for indexing or searching documents, resulting in a smaller overhead on the cluster.
87 |
88 | `alias` field declares alias settings. You can redirection the index without downtime.
89 |
90 | Now that we're ready, we can use eskeeper to create the index and alias.
91 |
92 | ```bash
93 | eskeeper < es.yaml
94 |
95 | curl localhost:9200/_cat/indices
96 | yellow open test-v1 ... 1 1 0 0 208b 208b
97 | yellow open test-v2 ... 1 1 0 0 208b 208b
98 | yellow close close-v1 ... 1 1
99 |
100 | curl localhost:9200/_cat/aliases
101 | alias2 test-v2 - - - -
102 | alias1 test-v1 - - - -
103 | alias2 test-v1 - - - -
104 | ```
105 |
106 | ## change index setting and switch alias
107 |
108 | Next, change mapping & config file.
109 |
110 | ```diff
111 | {
112 | "settings": {
113 | "number_of_shards": 1,
114 | "number_of_replicas": 1,
115 | "analysis": {
116 | "analyzer": {
117 | "test_analyzer": {
118 | "type": "custom",
119 | "tokenizer": "standard",
120 | "filter": ["lowercase"]
121 | }
122 | }
123 | }
124 | },
125 | "mappings": {
126 | "properties": {
127 | "id": {
128 | "type": "long"
129 | },
130 | "title": {
131 | "type": "text"
132 | },
133 | "body": {
134 | "type": "text",
135 | "analyzer": "test_analyzer"
136 | },
137 | + "category": {
138 | + "type": "text"
139 | + }
140 | }
141 | }
142 | ```
143 |
144 | mapping update is not supported. so you should create new index.
145 |
146 | ```diff
147 | index:
148 | - name: test-v1 # index name
149 | mapping: test.json # index setting & mapping (json)
150 |
151 | - name: test-v2
152 | mapping: test.json
153 |
154 | - name: close-v1
155 | mapping: test.json
156 | status: close
157 |
158 | + - name: test-v3
159 | + mapping: test.json
160 |
161 | alias:
162 | - name: alias1
163 | index:
164 | - test-v1
165 |
166 | # multi indicies
167 | - name: alias2
168 | index:
169 | - test-v1
170 | - - test-v2
171 | + - test-v3
172 | ```
173 |
174 | eskeeper creates new index ```test-v3``` with new mapping has `category` field. But, ```test-v1``` and ```test-v2``` setting & mapping is not changed (these index done not have `category` field).
175 |
176 | ```bash
177 | eskeeper < es.yaml
178 |
179 | curl localhost:9200/_cat/indices
180 | yellow open test-v1 ... 1 1 0 0 208b 208b
181 | yellow open test-v2 ... 1 1 0 0 208b 208b
182 | yellow open test-v3 ... 1 1 0 0 208b 208b
183 | yellow close close-v1 ... 1 1
184 |
185 | curl localhost:9200/_cat/aliases
186 | alias2 test-v3 - - - -
187 | alias1 test-v1 - - - -
188 | alias2 test-v1 - - - -
189 | ```
190 |
191 | ## reindex
192 |
193 | Finally, let's try reindex. put data to `test-v3` index.
194 |
195 | ```
196 | curl -X POST localhost:9200/test-v3/_doc -H "Content-Type: application/json" -d '{
197 | "id": 1,
198 | "title": "how to use eskeeper",
199 | "body": "this is A Tour of eskeeper",
200 | "category": "Elasticsearch"
201 | }'
202 | ```
203 |
204 | checks `test-v3` has one doc.
205 |
206 | ```
207 | curl localhost:9200/_cat/indices
208 | yellow open test-v1 ... 1 1 0 0 208b 208b
209 | yellow open test-v2 ... 1 1 0 0 208b 208b
210 | yellow open test-v3 ... 1 1 1 0 4.1kb 4.1kb
211 | yellow close close-v1 xxxxxxxxxxxx 1 1
212 | ```
213 |
214 | To reidnex `test-v3` to new index `test-v4`, use reindex option.
215 |
216 | ```diff
217 | index:
218 | - name: test-v1 # index name
219 | mapping: test.json # index setting & mapping (json)
220 |
221 | - name: test-v2
222 | mapping: test.json
223 |
224 | - name: close-v1
225 | mapping: test.json
226 | status: close
227 |
228 | - name: test-v3
229 | mapping: test.json
230 |
231 | + - name: test-v4
232 | + mapping: test.json
233 | + reindex:
234 | + source: test-v3
235 | + waitForCompletion: false
236 | + on: firstCreated
237 |
238 | alias:
239 | - name: alias1
240 | index:
241 | - test-v1
242 |
243 | # multi indicies
244 | - name: alias2
245 | index:
246 | - test-v1
247 | - - test-v3
248 | + - test-v4
249 | ```
250 |
251 | reindex field supports `source`, `slices`, `waitForComletion`, `on` options. `source`, `slices`, `waitForComletion` is same as [Reindex API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html) request body options.
252 |
253 | `on` field is eskeeper original option. If `on: firstCreated` is specified, reindex will only be executed the first time an index is created. If `on: always` is specified, a reindex will be run each time eskeeper is run.
254 |
255 | ```bash
256 | eskeeper < es.yaml
257 |
258 | curl localhost:9200/_cat/indices
259 | yellow open test-v1 ... 1 1 0 0 208b 208b
260 | yellow open test-v2 ... 1 1 0 0 208b 208b
261 | yellow open test-v3 ... 1 1 1 0 4.1kb 4.1kb
262 | yellow open test-v4 ... 1 1 1 0 4.1kb 4.1kb
263 | yellow close close-v1 xxxxxxxxxxxx 1 1
264 |
265 | curl localhost:9200/_cat/aliases
266 | alias2 test-v4 - - - -
267 | alias1 test-v1 - - - -
268 | alias2 test-v1 - - - -
269 | ```
270 |
271 | reindex is completed. check that `test-v4` has one doc.
272 |
--------------------------------------------------------------------------------
/example/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | elasticsearch:
4 | image: docker.elastic.co/elasticsearch/elasticsearch:7.15.1
5 | ports:
6 | - "9200:9200"
7 | - "9300:9300"
8 | environment:
9 | - discovery.type=single-node
10 |
--------------------------------------------------------------------------------
/example/es.yaml:
--------------------------------------------------------------------------------
1 | index:
2 | - name: test-v1 # index name
3 | mapping: test.json # index setting & mapping (json)
4 |
5 | - name: test-v2
6 | mapping: test.json
7 |
8 | - name: close-v1
9 | mapping: test.json
10 | status: close
11 |
12 | # - name: test-v3
13 | # mapping: test.json
14 |
15 | # - name: test-v4
16 | # mapping: test.json
17 | # reindex:
18 | # source: test-v3
19 | # waitForCompletion: false
20 | # on: firstCreated
21 |
22 | alias:
23 | - name: alias1
24 | index:
25 | - test-v1
26 |
27 | # multi indicies
28 | - name: alias2
29 | index:
30 | - test-v1
31 | - test-v2
32 |
--------------------------------------------------------------------------------
/example/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "number_of_shards": 1,
4 | "number_of_replicas": 1,
5 | "analysis": {
6 | "analyzer": {
7 | "test_analyzer": {
8 | "type": "custom",
9 | "tokenizer": "standard",
10 | "filter": ["lowercase"]
11 | }
12 | }
13 | }
14 | },
15 | "mappings": {
16 | "properties": {
17 | "id": {
18 | "type": "long"
19 | },
20 | "title": {
21 | "type": "text"
22 | },
23 | "body": {
24 | "type": "text",
25 | "analyzer": "test_analyzer"
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/po3rin/eskeeper
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/Cside/jsondiff v0.0.0-20180209072652-0e50d980b458
7 | github.com/Masterminds/semver/v3 v3.1.1 // indirect
8 | github.com/avast/retry-go v3.0.0+incompatible // indirect
9 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect
10 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect
11 | github.com/cenkalti/backoff/v4 v4.1.1 // indirect
12 | github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect
13 | github.com/creack/pty v1.1.9 // indirect
14 | github.com/elastic/go-elasticsearch v0.0.0
15 | github.com/elastic/go-elasticsearch/v7 v7.11.0
16 | github.com/evanphx/json-patch v4.9.0+incompatible // indirect
17 | github.com/fatih/color v1.10.0 // indirect
18 | github.com/goccy/go-yaml v1.8.9
19 | github.com/gofrs/uuid v4.0.0+incompatible
20 | github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e // indirect
21 | github.com/itchyny/astgen-go v0.0.0-20200815150004-12a293722290 // indirect
22 | github.com/itchyny/gojq v0.12.2
23 | github.com/kataras/pio v0.0.10
24 | github.com/mattn/go-colorable v0.1.8 // indirect
25 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
26 | github.com/oligot/go-mod-upgrade v0.4.1 // indirect
27 | github.com/ory/dockertest v3.3.5+incompatible
28 | github.com/ory/dockertest/v3 v3.6.0 // indirect
29 | github.com/pkg/errors v0.9.1
30 | github.com/po3rin/bmfzf v0.0.2
31 | github.com/sergi/go-diff v1.1.0 // indirect
32 | github.com/sirupsen/logrus v1.7.0 // indirect
33 | github.com/spf13/cobra v1.1.3
34 | github.com/spf13/pflag v1.0.5
35 | github.com/spf13/viper v1.7.1
36 | github.com/stretchr/testify v1.6.1 // indirect
37 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
38 | golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18 // indirect
39 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
40 | golang.org/x/text v0.3.5 // indirect
41 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
42 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
43 | gopkg.in/go-playground/validator.v9 v9.30.0 // indirect
44 | gopkg.in/yaml.v2 v2.4.0
45 | )
46 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
4 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
5 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
6 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
9 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
10 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
11 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
12 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
13 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
14 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
15 | github.com/AlecAivazis/survey/v2 v2.2.9 h1:LWvJtUswz/W9/zVVXELrmlvdwWcKE60ZAw0FWV9vssk=
16 | github.com/AlecAivazis/survey/v2 v2.2.9/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
17 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
18 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
19 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
20 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
21 | github.com/Cside/jsondiff v0.0.0-20180209072652-0e50d980b458 h1:qmUvmwHdHBWSICS5F1lWf2A5B20QkueQXluuGp/5gnY=
22 | github.com/Cside/jsondiff v0.0.0-20180209072652-0e50d980b458/go.mod h1:FSe4ejBYCH1Cwo9T60GhzjKATA8f/qvmfEn6N3W985s=
23 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
24 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
25 | github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
26 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
27 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
28 | github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
29 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
30 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
31 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
32 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
33 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
34 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
35 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
36 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
37 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
38 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
39 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
40 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
41 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
42 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
43 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
44 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
45 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
46 | github.com/cenkalti/backoff v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE=
47 | github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
48 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
49 | github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
50 | github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
51 | github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
52 | github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
53 | github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ=
54 | github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
55 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
56 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
57 | github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw=
58 | github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
59 | github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a h1:jEIoR0aA5GogXZ8pP3DUzE+zrhaF6/1rYZy+7KkYEWM=
60 | github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=
61 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
62 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
63 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
64 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
65 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
66 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
67 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
68 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
69 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
70 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
71 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
72 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
73 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
74 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
75 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
76 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
77 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
78 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
79 | github.com/elastic/go-elasticsearch v0.0.0 h1:Pd5fqOuBxKxv83b0+xOAJDAkziWYwFinWnBO0y+TZaA=
80 | github.com/elastic/go-elasticsearch v0.0.0/go.mod h1:TkBSJBuTyFdBnrNqoPc54FN0vKf5c04IdM4zuStJ7xg=
81 | github.com/elastic/go-elasticsearch/v7 v7.9.0 h1:UEau+a1MiiE/F+UrDj60kqIHFWdzU1M2y/YtBU2NC2M=
82 | github.com/elastic/go-elasticsearch/v7 v7.9.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
83 | github.com/elastic/go-elasticsearch/v7 v7.11.0 h1:bv+2GqsVrPdX/ChJqAHAFtWgtGvVJ0icN/WdBGAdNuw=
84 | github.com/elastic/go-elasticsearch/v7 v7.11.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
85 | github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
86 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
87 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
88 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
89 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
90 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
91 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
92 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
93 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
94 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
95 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
96 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
97 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
98 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
99 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
100 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
101 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
102 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
103 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
104 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
105 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
106 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
107 | github.com/goccy/go-yaml v1.8.2 h1:gDYrSN12XK/wQTFjxWIgcIqjNCV/Zb5V09M7cq+dbCs=
108 | github.com/goccy/go-yaml v1.8.2/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y=
109 | github.com/goccy/go-yaml v1.8.9 h1:4AEXg2qx+/w29jXnXpMY6mTckmYu1TMoHteKuMf0HFg=
110 | github.com/goccy/go-yaml v1.8.9/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
111 | github.com/gofrs/uuid v1.2.0 h1:coDhrjgyJaglxSjxuJdqQSSdUpG3w6p1OwN2od6frBU=
112 | github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
113 | github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
114 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
115 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
116 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
117 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
118 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
119 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
120 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
121 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
122 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
123 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
124 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
125 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
126 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
127 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
128 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
129 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
130 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
131 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
132 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
133 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
134 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
135 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
136 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
137 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
138 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
139 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
140 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
141 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
142 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
143 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
144 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
145 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
146 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
147 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
148 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
149 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
150 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
151 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
152 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
153 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
154 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
155 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
156 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
157 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
158 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
159 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
160 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
161 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
162 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
163 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
164 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
165 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
166 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
167 | github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
168 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
169 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
170 | github.com/itchyny/astgen-go v0.0.0-20200815150004-12a293722290/go.mod h1:296z3W7Xsrp2mlIY88ruDKscuvrkL6zXCNRtaYVshzw=
171 | github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA=
172 | github.com/itchyny/gojq v0.11.1 h1:k54XkzWCGDfRJSZFRW4rXowTVzPlSjU2xUErkaFjfdo=
173 | github.com/itchyny/gojq v0.11.1/go.mod h1:8MKtgvJwkmRduSuzN25byPdNHfvv6y+/hmOVXei9e7k=
174 | github.com/itchyny/gojq v0.11.2 h1:lKhMKfH7fTKMWj2Zr8az/9TliCn0TTXVc/BXfQ8Jhfc=
175 | github.com/itchyny/gojq v0.11.2/go.mod h1:XtmtF1PxeDpwLC1jyz/xAmV78ANlP0S9LVEPsKweK0A=
176 | github.com/itchyny/gojq v0.12.2 h1:TxhFjk1w7Vnb0SwQPeG4FxTC98O4Es+x/mPaD5Azgfs=
177 | github.com/itchyny/gojq v0.12.2/go.mod h1:mi4PdXSlFllHyByM68JKUrbiArtEdEnNEmjbwxcQKAg=
178 | github.com/itchyny/timefmt-go v0.1.0/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
179 | github.com/itchyny/timefmt-go v0.1.1 h1:rLpnm9xxb39PEEVzO0n4IRp0q6/RmBc7Dy/rE4HrA0U=
180 | github.com/itchyny/timefmt-go v0.1.1/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
181 | github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
182 | github.com/itchyny/timefmt-go v0.1.2/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
183 | github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e h1:ZZCvgaRDZg1gC9/1xrsgaJzQUCQgniKtw0xjWywWAOE=
184 | github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e/go.mod h1:+rHyWac2R9oAZwFe1wGY2HBzFJJy++RHBg1cU23NkD8=
185 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
186 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
187 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
188 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
189 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
190 | github.com/kataras/pio v0.0.10 h1:b0qtPUqOpM2O+bqa5wr2O6dN4cQNwSmFd6HQqgVae0g=
191 | github.com/kataras/pio v0.0.10/go.mod h1:gS3ui9xSD+lAUpbYnjOGiQyY7sUMJO+EHpiRzhtZ5no=
192 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
193 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
194 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
195 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
196 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
197 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
198 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
199 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
200 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
201 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
202 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
203 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
204 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
205 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
206 | github.com/ktr0731/go-fuzzyfinder v0.2.1 h1:YR9LXobzd9N+RVU9j4ASc0kWktTyJnkTex8Y6TW99f0=
207 | github.com/ktr0731/go-fuzzyfinder v0.2.1/go.mod h1:1BUWoT8siOp5n8ns8S6rtfTVigx/dvPPUJuu79nixgo=
208 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
209 | github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
210 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
211 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
212 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
213 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
214 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
215 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
216 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
217 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
218 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
219 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
220 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
221 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
222 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
223 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
224 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
225 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
226 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
227 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
228 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
229 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
230 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
231 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
232 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
233 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
234 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
235 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
236 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
237 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
238 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
239 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
240 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
241 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
242 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
243 | github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
244 | github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
245 | github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 h1:SPoLlS9qUUnXcIY4pvA4CTwYjk0Is5f4UPEkeESr53k=
246 | github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
247 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
248 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
249 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
250 | github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag=
251 | github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
252 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
253 | github.com/oligot/go-mod-upgrade v0.4.1 h1:dc7TeRDPclyVKnEh4O1VzMzRkoer5cTwZIFpNIwfbOA=
254 | github.com/oligot/go-mod-upgrade v0.4.1/go.mod h1:sKrcfERdiA1WOfY5S24RteSgxfk3wnfHTQxuHU/u81E=
255 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
256 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
257 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
258 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
259 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
260 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
261 | github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
262 | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
263 | github.com/opencontainers/runc v1.0.0-rc9 h1:/k06BMULKF5hidyoZymkoDCzdJzltZpz/UU4LguQVtc=
264 | github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
265 | github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA=
266 | github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
267 | github.com/ory/dockertest/v3 v3.6.0/go.mod h1:4ZOpj8qBUmh8fcBSVzkH2bws2s91JdGvHUqan4GHEuQ=
268 | github.com/ory/dockertest/v3 v3.6.1 h1:gxheDizSurXBKRj1Lu5Ws/xzQx35VgzkLFr1woC6Hoc=
269 | github.com/ory/dockertest/v3 v3.6.1/go.mod h1:EFLcVUOl8qCwp9NyDAcCDtq/QviLtYswW/VbWzUnTNE=
270 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
271 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
272 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
273 | github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
274 | github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
275 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
276 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
277 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
278 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
279 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
280 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
281 | github.com/po3rin/bmfzf v0.0.1 h1:O8hP4rTevvk/jl9i5yaDQHbTOKOAaAy1b++oqOPOzo4=
282 | github.com/po3rin/bmfzf v0.0.1/go.mod h1:eAoLgvjpJiJU7OPP+O0LDM1o4Uyv/bcEST7+rWROXKw=
283 | github.com/po3rin/bmfzf v0.0.2 h1:jEeTaIOdvewWNqsyoeMDHiCvexAPMJIoX8/yFemfwLI=
284 | github.com/po3rin/bmfzf v0.0.2/go.mod h1:z5yDQ+AXqRkQ9zFblk/QbSn/sldI2HAouDSsBsc1YOQ=
285 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
286 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
287 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
288 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
289 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
290 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
291 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
292 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
293 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
294 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
295 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
296 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
297 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
298 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
299 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
300 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
301 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
302 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
303 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
304 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
305 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
306 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
307 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
308 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
309 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
310 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
311 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
312 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
313 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
314 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
315 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
316 | github.com/spf13/afero v1.3.1 h1:GPTpEAuNr98px18yNQ66JllNil98wfRZ/5Ukny8FeQA=
317 | github.com/spf13/afero v1.3.1/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
318 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
319 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
320 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
321 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
322 | github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
323 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
324 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
325 | github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
326 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
327 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
328 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
329 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
330 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
331 | github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
332 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
333 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
334 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
335 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
336 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
337 | github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
338 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
339 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
340 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
341 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
342 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
343 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
344 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
345 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
346 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
347 | github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
348 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
349 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
350 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
351 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
352 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
353 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
354 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
355 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
356 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
357 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
358 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
359 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
360 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
361 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
362 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
363 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
364 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
365 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
366 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
367 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
368 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
369 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
370 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
371 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
372 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
373 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
374 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
375 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
376 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
377 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
378 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
379 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
380 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
381 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
382 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
383 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
384 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
385 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
386 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
387 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
388 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
389 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
390 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
391 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
392 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
393 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
394 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
395 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
396 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
397 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
398 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
399 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
400 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
401 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
402 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
403 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
404 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
405 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
406 | golang.org/x/net v0.0.0-20191003171128-d98b1b443823 h1:Ypyv6BNJh07T1pUSrehkLemqPKXhus2MkfktJ91kRh4=
407 | golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
408 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY=
409 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
410 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
411 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
412 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
413 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
414 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
415 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
416 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
417 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
418 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
419 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
420 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
421 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
422 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
423 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
424 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
425 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
426 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
427 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
428 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
429 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
430 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
431 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
432 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
433 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
434 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
435 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
436 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
437 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
438 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
439 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
440 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
441 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
442 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
443 | golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
444 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
445 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
446 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
447 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
448 | golang.org/x/sys v0.0.0-20200821140526-fda516888d29/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
449 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
450 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
451 | golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
452 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
453 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
454 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
455 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
456 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
457 | golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
458 | golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18 h1:jxr7/dEo+rR29uEBoLSWJ1tRHCFAMwFbGUU9nRqzpds=
459 | golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
460 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
461 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
462 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
463 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
464 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
465 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
466 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
467 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
468 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
469 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
470 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
471 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
472 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
473 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
474 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
475 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
476 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
477 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
478 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
479 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
480 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
481 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
482 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
483 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
484 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
485 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
486 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
487 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
488 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
489 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
490 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
491 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
492 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
493 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
494 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
495 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
496 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
497 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
498 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
499 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
500 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
501 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
502 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
503 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
504 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
505 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
506 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
507 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
508 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
509 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
510 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
511 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
512 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
513 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
514 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
515 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
516 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
517 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
518 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
519 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
520 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
521 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
522 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
523 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
524 | gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
525 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
526 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
527 | gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
528 | gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
529 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
530 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
531 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
532 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
533 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
534 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
535 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
536 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
537 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
538 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
539 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
540 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
541 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
542 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
543 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
544 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
545 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
546 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
547 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
548 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
549 |
--------------------------------------------------------------------------------
/index.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | )
9 |
10 | func (c *esclient) existIndex(ctx context.Context, index string) (bool, error) {
11 | exists := c.client.Indices.Exists
12 |
13 | res, err := exists(
14 | []string{index},
15 | exists.WithContext(ctx),
16 | )
17 | if err != nil {
18 | return false, fmt.Errorf("check index exists: %w", err)
19 | }
20 | if res.StatusCode == 200 {
21 | return true, nil
22 | }
23 | return false, nil
24 | }
25 |
26 | func (c *esclient) syncIndex(ctx context.Context, index index) error {
27 | create := c.client.Indices.Create
28 | ok, err := c.existIndex(ctx, index.Name)
29 | if err != nil {
30 | return err
31 | }
32 |
33 | // index dose not exist.
34 | if !ok {
35 | f, err := os.Open(index.Mapping)
36 | if err != nil {
37 | return fmt.Errorf("open mapping file: %w", err)
38 | }
39 |
40 | res, err := create(
41 | index.Name,
42 | create.WithContext(ctx),
43 | create.WithBody(f),
44 | )
45 | if err != nil {
46 | return fmt.Errorf("create index: %w", err)
47 | }
48 | if res.StatusCode != 200 {
49 | body, err := ioutil.ReadAll(res.Body)
50 | if err != nil {
51 | return fmt.Errorf("failed to create index [index=%v, statusCode=%v]", index.Name, res.StatusCode)
52 | }
53 | return fmt.Errorf("failed to create index [index=%v, statusCode=%v, res=%v]", index.Name, res.StatusCode, string(body))
54 | }
55 |
56 | // reindex --------
57 |
58 | if index.Reindex.Source == "" {
59 | return nil
60 | }
61 |
62 | ok, err := c.existIndex(ctx, index.Reindex.Source)
63 | if err != nil {
64 | return fmt.Errorf("check index exists for reindex process: %w", err)
65 | }
66 | if !ok {
67 | return fmt.Errorf("reindex (%s -> %s) conf is invalid. Make sure %s index exists", index.Reindex.Source, index.Reindex.Source, index.Name)
68 | }
69 |
70 | err = c.reindex(ctx, index.Name, index.Reindex)
71 | if err != nil {
72 | return fmt.Errorf("reindex (%s -> %s)", index.Reindex.Source, index.Name)
73 | }
74 | return nil
75 | }
76 |
77 | // index already exists.
78 |
79 | // reindex -------
80 | if index.Reindex.Source != "" && index.Reindex.On == "always" {
81 | ok, err = c.existIndex(ctx, index.Reindex.Source)
82 | if err != nil {
83 | return fmt.Errorf("check index exists for reindex process: %w", err)
84 | }
85 | if !ok {
86 | return fmt.Errorf("reindex (%s -> %s) conf is invalid. Make sure %s index exists", index.Reindex.Source, index.Reindex.Source, index.Name)
87 | }
88 | err = c.reindex(ctx, index.Name, index.Reindex)
89 | if err != nil {
90 | return fmt.Errorf("reindex (%s -> %s)", index.Reindex.Source, index.Name)
91 | }
92 | return nil
93 | }
94 |
95 | // Since downtime may occur when switching aliases, only open is processed before switching aliases.
96 | // TODO: refactoring.
97 | if index.Status == "close" {
98 | return nil
99 | }
100 | err = c.openIndex(ctx, index)
101 | if err != nil {
102 | return fmt.Errorf("open index: %w", err)
103 | }
104 | return nil
105 | }
106 |
107 | func (c *esclient) deleteIndex(ctx context.Context, index string) error {
108 | delete := c.client.Indices.Delete
109 | res, err := delete([]string{index}, delete.WithContext(ctx))
110 | if err != nil {
111 | return fmt.Errorf("delete index: %w", err)
112 | }
113 | if res.StatusCode != 200 {
114 | body, err := ioutil.ReadAll(res.Body)
115 | if err != nil {
116 | return fmt.Errorf("failed to delete index [index= %v, statusCode=%v]", index, res.StatusCode)
117 | }
118 | return fmt.Errorf("failed to delete index [index= %v, statusCode=%v, res=%v]", index, res.StatusCode, string(body))
119 | }
120 | return nil
121 | }
122 |
123 | func (c *esclient) syncCloseStatus(ctx context.Context, conf config) error {
124 | for _, index := range conf.Indices {
125 | if index.Status == "close" {
126 | err := c.closeIndex(ctx, index)
127 | if err != nil {
128 | return fmt.Errorf("crearted index status action: %w", err)
129 | }
130 | }
131 | }
132 | return nil
133 | }
134 |
135 | // func (c *esclient) indexStatusAction(ctx context.Context, index index) error {
136 | // switch index.Status {
137 | // case "close":
138 | // err := c.closeIndex(ctx, index)
139 | // if err != nil {
140 | // return fmt.Errorf("close index: %w", err)
141 | // }
142 | // case "open":
143 | // err := c.openIndex(ctx, index)
144 | // if err != nil {
145 | // return fmt.Errorf("open index: %w", err)
146 | // }
147 | // default:
148 | // err := c.openIndex(ctx, index)
149 | // if err != nil {
150 | // return fmt.Errorf("open index: %w", err)
151 | // }
152 | // }
153 | // return nil
154 | // }
155 |
156 | func (c *esclient) closeIndex(ctx context.Context, index index) error {
157 | close := c.client.Indices.Close
158 | res, err := close([]string{index.Name}, close.WithContext(ctx))
159 | if err != nil {
160 | return fmt.Errorf("close index: %w", err)
161 | }
162 | if res.StatusCode != 200 {
163 | body, err := ioutil.ReadAll(res.Body)
164 | if err != nil {
165 | return fmt.Errorf("failed to close index [index= %v, statusCode=%v]", index, res.StatusCode)
166 | }
167 | return fmt.Errorf("failed to close index [index= %v, statusCode=%v, res=%v]", index, res.StatusCode, string(body))
168 | }
169 | return nil
170 | }
171 |
172 | func (c *esclient) openIndex(ctx context.Context, index index) error {
173 | open := c.client.Indices.Open
174 | res, err := open([]string{index.Name}, open.WithContext(ctx))
175 | if err != nil {
176 | return fmt.Errorf("open index: %w", err)
177 | }
178 | if res.StatusCode != 200 {
179 | body, err := ioutil.ReadAll(res.Body)
180 | if err != nil {
181 | return fmt.Errorf("failed to open index [index= %v, statusCode=%v]: %w", index, res.StatusCode, err)
182 | }
183 | return fmt.Errorf("failed to open index [index= %v, statusCode=%v, res=%v]: %w", index, res.StatusCode, string(body), err)
184 | }
185 | return nil
186 | }
187 |
188 | func (c *esclient) syncIndices(ctx context.Context, conf config) error {
189 | for _, index := range conf.Indices {
190 | err := c.syncIndex(ctx, index)
191 | if err != nil {
192 | c.logf("[fail] index: %v\n", index.Name)
193 | return fmt.Errorf("sync index: %w", err)
194 | }
195 | c.logf("[synced] index: %v\n", index.Name)
196 | }
197 | return nil
198 | }
199 |
--------------------------------------------------------------------------------
/index_test.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | func TestSyncIndices(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | conf config
12 | setup func(tb testing.TB)
13 | cleanup func(tb testing.TB)
14 | }{
15 | {
16 | name: "simple",
17 | conf: config{
18 | Indices: []index{
19 | {
20 | Name: "create1",
21 | Mapping: "testdata/test.json",
22 | },
23 | },
24 | },
25 | cleanup: func(tb testing.TB) {
26 | deleteIndexHelper(tb, []string{"create1"})
27 | },
28 | },
29 | {
30 | name: "multi",
31 | conf: config{
32 | Indices: []index{
33 | {
34 | Name: "create2",
35 | Mapping: "testdata/test.json",
36 | },
37 | {
38 | Name: "create3",
39 | Mapping: "testdata/test.json",
40 | },
41 | },
42 | },
43 | cleanup: func(tb testing.TB) {
44 | deleteIndexHelper(tb, []string{"create2", "create3"})
45 | },
46 | },
47 | {
48 | name: "idempotence",
49 | conf: config{
50 | Indices: []index{
51 | {
52 | Name: "idempotence",
53 | Mapping: "testdata/test.json",
54 | },
55 | {
56 | Name: "idempotence",
57 | Mapping: "testdata/test.json",
58 | },
59 | },
60 | },
61 | cleanup: func(tb testing.TB) {
62 | deleteIndexHelper(tb, []string{"idempotence"})
63 | },
64 | },
65 | {
66 | name: "close",
67 | conf: config{
68 | Indices: []index{
69 | {
70 | Name: "create-with-close-v1",
71 | Mapping: "testdata/test.json",
72 | Status: "close",
73 | },
74 | },
75 | },
76 | cleanup: func(tb testing.TB) {
77 | deleteIndexHelper(tb, []string{"create-with-close-v1"})
78 | },
79 | },
80 | {
81 | name: "exists close",
82 | conf: config{
83 | Indices: []index{
84 | {
85 | Name: "create-with-close-v2",
86 | Mapping: "testdata/test.json",
87 | Status: "close",
88 | },
89 | },
90 | },
91 | setup: func(tb testing.TB) {
92 | createTmpIndexHelper(tb, "create-with-close-v2")
93 | },
94 | cleanup: func(tb testing.TB) {
95 | deleteIndexHelper(tb, []string{"create-with-close-v2"})
96 | },
97 | },
98 | {
99 | name: "open",
100 | conf: config{
101 | Indices: []index{
102 | {
103 | Name: "open-v1",
104 | Mapping: "testdata/test.json",
105 | },
106 | },
107 | },
108 | setup: func(tb testing.TB) {
109 | createTmpIndexHelper(tb, "open-v1")
110 | closeIndexHelper(tb, "open-v1")
111 | },
112 | cleanup: func(tb testing.TB) {
113 | deleteIndexHelper(tb, []string{"open-v1"})
114 | },
115 | },
116 | {
117 | name: "open-already-open",
118 | conf: config{
119 | Indices: []index{
120 | {
121 | Name: "open-already-open-v1",
122 | Mapping: "testdata/test.json",
123 | },
124 | },
125 | },
126 | cleanup: func(tb testing.TB) {
127 | deleteIndexHelper(tb, []string{"open-already-open-v1"})
128 | },
129 | },
130 | {
131 | name: "reindex",
132 | conf: config{
133 | Indices: []index{
134 | {
135 | Name: "reindex-v1",
136 | Mapping: "testdata/test.json",
137 | Reindex: reindex{
138 | Source: "reindex-v0",
139 | Slices: 3,
140 | WaitForCompletion: true,
141 | On: "firstCreated",
142 | },
143 | },
144 | },
145 | },
146 | setup: func(tb testing.TB) {
147 | createTmpIndexHelper(tb, "reindex-v0")
148 | postDocHelper(tb, "reindex-v0")
149 | },
150 | cleanup: func(tb testing.TB) {
151 | deleteIndexHelper(tb, []string{"reindex-v0", "reindex-v1"})
152 | },
153 | },
154 | {
155 | name: "reindex-already-exists",
156 | conf: config{
157 | Indices: []index{
158 | {
159 | Name: "reindex-exists",
160 | Mapping: "testdata/test.json",
161 | Reindex: reindex{
162 | Source: "reindex-v0",
163 | Slices: 3,
164 | WaitForCompletion: true,
165 | On: "firstCreated",
166 | },
167 | },
168 | },
169 | },
170 | setup: func(tb testing.TB) {
171 | createTmpIndexHelper(tb, "reindex-already-exists")
172 | createTmpIndexHelper(tb, "reindex-v0")
173 | postDocHelper(tb, "reindex-v0")
174 | },
175 | cleanup: func(tb testing.TB) {
176 | deleteIndexHelper(tb, []string{"reindex-already-exists", "reindex-v0"})
177 | },
178 | },
179 | }
180 |
181 | es, err := newEsClient([]string{url}, "", "")
182 | if err != nil {
183 | t.Fatal(err)
184 | }
185 |
186 | for _, tt := range tests {
187 | t.Run(tt.name, func(t *testing.T) {
188 | ctx := context.Background()
189 | if tt.setup != nil {
190 | tt.setup(t)
191 | }
192 | err := es.syncIndices(ctx, tt.conf)
193 | if err != nil {
194 | t.Error(err)
195 | }
196 | if tt.cleanup != nil {
197 | tt.cleanup(t)
198 | }
199 | })
200 | }
201 | }
202 |
203 | func TestExistIndex(t *testing.T) {
204 | tests := []struct {
205 | name string
206 | index string
207 | setup func(tb testing.TB)
208 | want bool
209 | cleanup func(tb testing.TB)
210 | }{
211 | {
212 | name: "simple",
213 | index: "exist-v1",
214 | setup: func(tb testing.TB) {
215 | createTmpIndexHelper(tb, "exist-v1")
216 | },
217 | want: true,
218 | },
219 | {
220 | name: "not-found",
221 | index: "exist-v2",
222 | setup: func(tb testing.TB) {
223 | },
224 | want: false,
225 | },
226 | }
227 |
228 | es, err := newEsClient([]string{url}, "", "")
229 | if err != nil {
230 | t.Fatal(err)
231 | }
232 |
233 | for _, tt := range tests {
234 | t.Run(tt.name, func(t *testing.T) {
235 | ctx := context.Background()
236 | tt.setup(t)
237 | ok, err := es.existIndex(ctx, tt.index)
238 | if err != nil {
239 | t.Error(err)
240 | }
241 | if ok != tt.want {
242 | t.Errorf("want: %+v, got: %+v\n", tt.want, ok)
243 | }
244 | })
245 | }
246 | }
247 |
248 | func TestDeleteIndex(t *testing.T) {
249 | tests := []struct {
250 | name string
251 | index string
252 | setup func(tb testing.TB)
253 | cleanup func(tb testing.TB)
254 | }{
255 | {
256 | name: "simple",
257 | index: "delete-v1",
258 | setup: func(tb testing.TB) {
259 | createTmpIndexHelper(tb, "delete-v1")
260 | },
261 | },
262 | }
263 |
264 | es, err := newEsClient([]string{url}, "", "")
265 | if err != nil {
266 | t.Fatal(err)
267 | }
268 |
269 | for _, tt := range tests {
270 | t.Run(tt.name, func(t *testing.T) {
271 | ctx := context.Background()
272 | tt.setup(t)
273 | err := es.deleteIndex(ctx, tt.index)
274 | if err != nil {
275 | t.Error(err)
276 | }
277 | })
278 | }
279 | }
280 |
281 | func TestSyncCloseStatus(t *testing.T) {
282 | tests := []struct {
283 | name string
284 | conf config
285 | setup func(tb testing.TB)
286 | }{
287 | {
288 | name: "exists close",
289 | conf: config{
290 | Indices: []index{
291 | {
292 | Name: "sync-close-v1",
293 | Mapping: "testdata/test.json",
294 | Status: "close",
295 | },
296 | },
297 | },
298 | setup: func(tb testing.TB) {
299 | createTmpIndexHelper(tb, "sync-close-v1")
300 | },
301 | },
302 | }
303 |
304 | es, err := newEsClient([]string{url}, "", "")
305 | if err != nil {
306 | t.Fatal(err)
307 | }
308 |
309 | for _, tt := range tests {
310 | t.Run(tt.name, func(t *testing.T) {
311 | ctx := context.Background()
312 | if tt.setup != nil {
313 | tt.setup(t)
314 | }
315 | err := es.syncCloseStatus(ctx, tt.conf)
316 | if err != nil {
317 | t.Error(err)
318 | }
319 | })
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/po3rin/eskeeper/6a6db07f7147afba9689bd18c67d024bdad9143b/logo.png
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/elastic/go-elasticsearch"
12 | "github.com/ory/dockertest"
13 | )
14 |
15 | var url string
16 |
17 | func TestMain(m *testing.M) {
18 | pool, err := dockertest.NewPool("")
19 | if err != nil {
20 | log.Fatalf("Could not connect to docker: %s", err)
21 | }
22 |
23 | resource, err := pool.Run(
24 | "docker.elastic.co/elasticsearch/elasticsearch",
25 | "7.11.1",
26 | []string{
27 | "ES_JAVA_OPTS=-Xms512m -Xmx512m",
28 | "discovery.type=single-node",
29 | "node.name=es01",
30 | },
31 | )
32 | if err != nil {
33 | log.Fatalf("Could not start resource: %s", err)
34 | }
35 |
36 | port := resource.GetPort("9200/tcp")
37 | url = fmt.Sprintf("http://localhost:%s", port)
38 |
39 | cfg := elasticsearch.Config{
40 | Addresses: []string{url},
41 | }
42 | es, err := elasticsearch.NewClient(cfg)
43 | if err != nil {
44 | log.Fatalf("Error creating the client: %s", err)
45 | }
46 |
47 | // exponential backoff-retry, because the application in the container might not be ready to accept connections yet
48 | if err := pool.Retry(func() error {
49 | res, err := es.Info()
50 | if err != nil {
51 | log.Println("waiting to be ready...")
52 | return err
53 | }
54 | defer res.Body.Close()
55 | return nil
56 | }); err != nil {
57 | log.Fatalf("could not retry to connect : %s\n", err)
58 | }
59 |
60 | code := m.Run()
61 |
62 | // You can't defer this because os.Exit doesn't care for defer
63 | if err := pool.Purge(resource); err != nil {
64 | log.Fatalf("Could not purge resource: %s", err)
65 | }
66 |
67 | os.Exit(code)
68 | }
69 |
70 | func createTmpIndexHelper(tb testing.TB, name string) {
71 | tb.Helper()
72 | conf := elasticsearch.Config{
73 | Addresses: []string{url},
74 | }
75 | es, err := elasticsearch.NewClient(conf)
76 | if err != nil {
77 | tb.Fatal(err)
78 | }
79 |
80 | f, err := os.Open("testdata/test.json")
81 | if err != nil {
82 | tb.Fatal(err)
83 | }
84 |
85 | res, err := es.Indices.Create(
86 | name,
87 | es.Indices.Create.WithBody(f),
88 | )
89 | if err != nil {
90 | tb.Fatal(err)
91 | }
92 |
93 | if res.StatusCode != 200 {
94 | body, err := ioutil.ReadAll(res.Body)
95 | if err != nil {
96 | tb.Fatal(err)
97 | }
98 | tb.Fatalf("failed to create index [index=%v, statusCode=%v, res=%v]", name, res.StatusCode, string(body))
99 | }
100 | }
101 |
102 | var testAliasQuery = `
103 | {
104 | "actions" : [
105 | { "add" : { "index" : "%v", "alias" : "%v" } }
106 | ]
107 | }`
108 |
109 | func createTmpAliasHelper(tb testing.TB, name string, index string) {
110 | tb.Helper()
111 | conf := elasticsearch.Config{
112 | Addresses: []string{url},
113 | }
114 | es, err := elasticsearch.NewClient(conf)
115 | if err != nil {
116 | tb.Fatal(err)
117 | }
118 |
119 | res, err := es.Indices.UpdateAliases(
120 | strings.NewReader(fmt.Sprintf(testAliasQuery, index, name)),
121 | )
122 | if err != nil {
123 | tb.Fatal(err)
124 | }
125 |
126 | if res.StatusCode != 200 {
127 | body, err := ioutil.ReadAll(res.Body)
128 | if err != nil {
129 | tb.Fatal(err)
130 | }
131 | tb.Fatalf("failed to create alias [index= %v, statusCode=%v, res=%v]", name, res.StatusCode, string(body))
132 | }
133 | }
134 | func deleteIndexHelper(tb testing.TB, indices []string) {
135 | conf := elasticsearch.Config{
136 | Addresses: []string{url},
137 | }
138 | es, err := elasticsearch.NewClient(conf)
139 | if err != nil {
140 | tb.Fatal(err)
141 | }
142 | d := es.Indices.Delete
143 | res, err := d(indices)
144 | if err != nil {
145 | tb.Fatalf("close index: %v", err)
146 | }
147 | if res.StatusCode != 200 {
148 | body, err := ioutil.ReadAll(res.Body)
149 | if err != nil {
150 | tb.Fatalf("failed to close index [statusCode=%v]", res.StatusCode)
151 | }
152 | tb.Fatalf("failed to close index [statusCode=%v, res=%v]", res.StatusCode, string(body))
153 | }
154 | }
155 |
156 | func closeIndexHelper(tb testing.TB, index string) {
157 | conf := elasticsearch.Config{
158 | Addresses: []string{url},
159 | }
160 | es, err := elasticsearch.NewClient(conf)
161 | if err != nil {
162 | tb.Fatal(err)
163 | }
164 | close := es.Indices.Close
165 | res, err := close([]string{index})
166 | if err != nil {
167 | tb.Fatalf("close index: %v", err)
168 | }
169 | if res.StatusCode != 200 {
170 | body, err := ioutil.ReadAll(res.Body)
171 | if err != nil {
172 | tb.Fatalf("failed to close index [index= %v, statusCode=%v]", index, res.StatusCode)
173 | }
174 | tb.Fatalf("failed to close index [index= %v, statusCode=%v, res=%v]", index, res.StatusCode, string(body))
175 | }
176 | }
177 |
178 | func postDocHelper(tb testing.TB, index string) {
179 | tb.Helper()
180 | conf := elasticsearch.Config{
181 | Addresses: []string{url},
182 | }
183 | es, err := elasticsearch.NewClient(conf)
184 | if err != nil {
185 | tb.Fatal(err)
186 | }
187 |
188 | body := strings.NewReader(`{"title":"this is title","body":"this is body"}`)
189 |
190 | res, err := es.Index(
191 | index,
192 | body,
193 | es.Index.WithRefresh("true"),
194 | )
195 | if err != nil {
196 | tb.Fatal(err)
197 | }
198 |
199 | if res.StatusCode != 201 {
200 | body, err := ioutil.ReadAll(res.Body)
201 | if err != nil {
202 | tb.Fatal(err)
203 | }
204 | tb.Fatalf("failed to post document [index=%v, statusCode=%v, res=%v]", index, res.StatusCode, string(body))
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/reindex.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/ioutil"
7 | "strings"
8 | )
9 |
10 | func (c *esclient) reindex(ctx context.Context, dest string, reindex reindex) error {
11 | ri := c.client.Reindex
12 | body := strings.NewReader(
13 | fmt.Sprintf(`
14 | {
15 | "source": {
16 | "index": "%s"
17 | },
18 | "dest": {
19 | "index": "%s"
20 | }
21 | }`,
22 | reindex.Source, dest,
23 | ),
24 | )
25 |
26 | slices := reindex.Slices
27 | if slices == 0 {
28 | slices = 1
29 | }
30 |
31 | res, err := ri(
32 | body,
33 | ri.WithContext(ctx),
34 | ri.WithSlices(slices),
35 | ri.WithWaitForCompletion(reindex.WaitForCompletion),
36 | )
37 | if err != nil {
38 | return fmt.Errorf("reindex: %w", err)
39 | }
40 | if res.StatusCode != 200 {
41 | body, err := ioutil.ReadAll(res.Body)
42 | if err != nil {
43 | return err
44 | }
45 | return fmt.Errorf("failed to reindex [index=%v, statusCode=%v, res=%v]", reindex.Source, res.StatusCode, string(body))
46 | }
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/reindex_test.go:
--------------------------------------------------------------------------------
1 | package eskeeper
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | func TestReindex(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | dest string
12 | reindex reindex
13 | setup func(tb testing.TB)
14 | cleanup func(tb testing.TB)
15 | }{
16 | {
17 | name: "reindex",
18 | dest: "reindex-dest",
19 | reindex: reindex{
20 | Source: "reindex-src",
21 | Slices: 3,
22 | WaitForCompletion: true,
23 | On: "firstCreated",
24 | },
25 | setup: func(tb testing.TB) {
26 | createTmpIndexHelper(tb, "reindex-src")
27 | createTmpIndexHelper(tb, "reindex-dest")
28 | postDocHelper(tb, "reindex-src")
29 | },
30 | cleanup: func(tb testing.TB) {
31 | deleteIndexHelper(tb, []string{"reindex-src", "reindex-dest"})
32 | },
33 | },
34 | }
35 |
36 | es, err := newEsClient([]string{url}, "", "")
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 |
41 | for _, tt := range tests {
42 | t.Run(tt.name, func(t *testing.T) {
43 | ctx := context.Background()
44 | if tt.setup != nil {
45 | tt.setup(t)
46 | }
47 | err := es.reindex(ctx, tt.dest, tt.reindex)
48 | if err != nil {
49 | t.Error(err)
50 | }
51 | if tt.cleanup != nil {
52 | tt.cleanup(t)
53 | }
54 | })
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/testdata/es.reindex.yaml:
--------------------------------------------------------------------------------
1 | index:
2 | - name: test-v1
3 | mapping: testdata/test.json
4 |
5 | # reindex test-v1 -> reindex-v1
6 | - name: reindex-v1
7 | mapping: testdata/test.json
8 | reindex:
9 | source: test-v1
10 | slices: 3 # default=1
11 | waitForCompletion: true
12 | # 'on' field supports 2 hooks.
13 | # 'reindex': only when index is created for the first time.
14 | # 'always': always exec reindex.
15 | on: firstCreated
16 |
17 |
18 | alias:
19 | - name: alias1
20 | index:
21 | - test-v2
22 |
23 |
--------------------------------------------------------------------------------
/testdata/es.yaml:
--------------------------------------------------------------------------------
1 | index:
2 | - name: test-v1 # index name
3 | mapping: testdata/test.json # index setting & mapping (json)
4 |
5 | - name: test-v2
6 | mapping: testdata/test.json
7 |
8 | - name: close-v1
9 | mapping: testdata/test.json
10 | status: close
11 |
12 | alias:
13 | - name: alias1
14 | index:
15 | - test-v1
16 |
17 | # multi indicies
18 | - name: alias2
19 | index:
20 | - test-v1
21 | - test-v2
22 |
--------------------------------------------------------------------------------
/testdata/invalid.json:
--------------------------------------------------------------------------------
1 | {
2 | "mappings": {
3 | "roperties": {
4 | "id": {
5 | "type": "long",
6 | "index": true
7 | },
8 | "title": {
9 | "type": "text"
10 | },
11 | "body": {
12 | "type": "text"
13 | }
14 | }
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/testdata/invalidUpdateIndex.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings":{
3 | "number_of_shards": 2,
4 | "number_of_replicas" : 3,
5 | "analysis":{
6 | "analyzer":{
7 | "my_analyzer":{
8 | "type":"custom",
9 | "tokenizer":"standard",
10 | "filter":[
11 | "lowercase"
12 | ]
13 | },
14 | "my_stop_analyzer":{
15 | "type":"custom",
16 | "tokenizer":"standard",
17 | "filter":[
18 | "lowercase",
19 | "english_stop"
20 | ]
21 | }
22 | },
23 | "filter":{
24 | "english_stop":{
25 | "type":"stop",
26 | "stopwords":"_english_"
27 | }
28 | }
29 | }
30 | },
31 | "mappings": {
32 | "properties": {
33 | "id": {
34 | "type": "long",
35 | "index": true
36 | },
37 | "title": {
38 | "type": "text"
39 | },
40 | "body": {
41 | "type": "text"
42 | }
43 | }
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/testdata/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings":{
3 | "number_of_shards": 1,
4 | "number_of_replicas" : 1,
5 | "analysis":{
6 | "analyzer":{
7 | "test_analyzer":{
8 | "type":"custom",
9 | "tokenizer":"standard",
10 | "filter":[
11 | "lowercase"
12 | ]
13 | },
14 | "my_stop_analyzer":{
15 | "type":"custom",
16 | "tokenizer":"standard",
17 | "filter":[
18 | "lowercase",
19 | "english_stop"
20 | ]
21 | }
22 | },
23 | "filter":{
24 | "english_stop":{
25 | "type":"stop",
26 | "stopwords":"_english_"
27 | }
28 | }
29 | }
30 | },
31 | "mappings": {
32 | "properties": {
33 | "id": {
34 | "type": "long",
35 | "index": true
36 | },
37 | "title": {
38 | "type": "text"
39 | },
40 | "body": {
41 | "type": "text"
42 | }
43 | }
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/testdata/updateIndex.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings":{
3 | "number_of_shards": 1,
4 | "number_of_replicas" : 1,
5 | "analysis":{
6 | "analyzer":{
7 | "my_analyzer":{
8 | "type":"custom",
9 | "tokenizer":"standard",
10 | "filter":[
11 | "lowercase"
12 | ]
13 | },
14 | "my_stop_analyzer":{
15 | "type":"custom",
16 | "tokenizer":"standard",
17 | "filter":[
18 | "lowercase",
19 | "english_stop"
20 | ]
21 | }
22 | },
23 | "filter":{
24 | "english_stop":{
25 | "type":"stop",
26 | "stopwords":"_english_"
27 | }
28 | }
29 | }
30 | },
31 | "mappings": {
32 | "properties": {
33 | "id": {
34 | "type": "long",
35 | "index": true
36 | },
37 | "title": {
38 | "type": "text"
39 | },
40 | "body": {
41 | "type": "text"
42 | },
43 | "append": {
44 | "type": "text"
45 | }
46 | }
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------