pax_global_header00006660000000000000000000000064147541464160014526gustar00rootroot0000000000000052 comment=7a136d3c6e34b4f60a60e92d2f04c18c5fc47708 alertmanager-irc-relay-0.5.1/000077500000000000000000000000001475414641600160605ustar00rootroot00000000000000alertmanager-irc-relay-0.5.1/.github/000077500000000000000000000000001475414641600174205ustar00rootroot00000000000000alertmanager-irc-relay-0.5.1/.github/workflows/000077500000000000000000000000001475414641600214555ustar00rootroot00000000000000alertmanager-irc-relay-0.5.1/.github/workflows/test.yml000066400000000000000000000006451475414641600231640ustar00rootroot00000000000000on: [push, pull_request] name: Test jobs: test: strategy: matrix: go-version: [1.15.x, 1.18.x, 1.19.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Test run: go test -v ./... alertmanager-irc-relay-0.5.1/.gitignore000066400000000000000000000000371475414641600200500ustar00rootroot00000000000000alertmanager-irc-relay /vendor alertmanager-irc-relay-0.5.1/CONTRIBUTING.md000066400000000000000000000021151475414641600203100ustar00rootroot00000000000000# How to Contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution, this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). alertmanager-irc-relay-0.5.1/Dockerfile000066400000000000000000000002271475414641600200530ustar00rootroot00000000000000FROM golang:1.16 WORKDIR /go/src/app COPY . . RUN go get -d -v ./... RUN go install -v ./... CMD ["alertmanager-irc-relay", "--config=/config.yml"] alertmanager-irc-relay-0.5.1/LICENSE000066400000000000000000000261361475414641600170750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. alertmanager-irc-relay-0.5.1/README.md000066400000000000000000000076101475414641600173430ustar00rootroot00000000000000# Alertmanager IRC Relay Alertmanager IRC Relay is a bot relaying [Prometheus](https://prometheus.io/) alerts to IRC. Alerts are received from Prometheus using [Webhooks](https://prometheus.io/docs/alerting/configuration/#webhook-receiver-) and are relayed to an IRC channel. ### Configuring and running the bot To configure and run the bot you need to create a YAML configuration file and pass it to the service. Running the service without a configuration will use the default test values and connect to a default IRC channel, which you probably do not want to do. Example configuration: ``` # Start the HTTP server receiving alerts from Prometheus Webhook binding to # this host/port. # http_host: localhost http_port: 8000 # Connect to this IRC host/port. # # Note: SSL is enabled by default, use "irc_use_ssl: no" to disable. # Set "irc_verify_ssl: no" to accept invalid SSL certificates (not recommended) irc_host: irc.example.com irc_port: 7000 # Optionally set the server password irc_host_password: myserver_password # Use this IRC nickname. irc_nickname: myalertbot # Password used to identify with NickServ irc_nickname_password: mynickserv_key # Use this IRC real name irc_realname: myrealname # Optionally pre-join certain channels. # # Note: If an alert is sent to a non # pre-joined channel the bot will join # that channel anyway before sending the message. Of course this cannot work # with password-protected channels. irc_channels: - name: "#mychannel" - name: "#myprivatechannel" password: myprivatechannel_key # Define how IRC messages should be sent. # # Send only one message when webhook data is received. # Note: By default a message is sent for each alert in the webhook data. msg_once_per_alert_group: no # # Use PRIVMSG instead of NOTICE (default) to send messages. # Note: Sending PRIVMSG from bots is bad practice, do not enable this unless # necessary (e.g. unless NOTICEs would weaken your channel moderation policies) use_privmsg: yes # Define how IRC messages should be formatted. # # The formatting is based on golang's text/template . msg_template: "Alert {{ .Labels.alertname }} on {{ .Labels.instance }} is {{ .Status }}" # Note: When sending only one message per alert group the default # msg_template is set to # "Alert {{ .GroupLabels.alertname }} for {{ .GroupLabels.job }} is {{ .Status }}" # Set the internal buffer size for alerts received but not yet sent to IRC. alert_buffer_size: 2048 # Patterns used to guess whether NickServ is asking us to IDENTIFY # Note: If you need to change this because the bot is not catching a request # from a rather common NickServ, please consider sending a PR to update the # default config instead. nickserv_identify_patterns: - "identify via /msg NickServ identify " - "type /msg NickServ IDENTIFY password" - "authenticate yourself to services with the IDENTIFY command" # Rarely NickServ or ChanServ is reached at a specific hostname. Specify an # override here nickserv_name: NickServ chanserv_name: ChanServ ``` Running the bot (assuming *$GOPATH* and *$PATH* are properly setup for go): ``` $ go install github.com/google/alertmanager-irc-relay $ alertmanager-irc-relay --config /path/to/your/config/file ``` The configuration file can reference environment variables. It is then possible to specify certain parameters directly when running the bot: ``` $ cat /path/to/your/config/file ... http_port: $MY_SERVICE_PORT ... irc_nickname_password: $NICKSERV_PASSWORD ... $ export MY_SERVICE_PORT=8000 NICKSERV_PASSWORD=mynickserv_key $ alertmanager-irc-relay --config /path/to/your/config/file ``` ### Prometheus configuration Prometheus can be configured following the official [Webhooks](https://prometheus.io/docs/alerting/configuration/#webhook-receiver-) documentation. The `url` must specify the IRC channel name that alerts should be sent to: ``` send_resolved: false url: http://localhost:8000/mychannel ``` alertmanager-irc-relay-0.5.1/backoff.go000066400000000000000000000054711475414641600200110ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "math" "math/rand" "time" "github.com/google/alertmanager-irc-relay/logging" ) type JitterFunc func(int) int type DelayerMaker interface { NewDelayer(float64, float64, time.Duration) Delayer } type Delayer interface { Delay() DelayContext(context.Context) bool } type Backoff struct { step float64 maxBackoff float64 resetDelta float64 lastAttempt time.Time durationUnit time.Duration jitterer JitterFunc timeTeller TimeTeller } func jitterFunc(input int) int { if input == 0 { return 0 } return rand.Intn(input) } type BackoffMaker struct{} func (bm *BackoffMaker) NewDelayer(maxBackoff float64, resetDelta float64, durationUnit time.Duration) Delayer { timeTeller := &RealTime{} return NewBackoffForTesting( maxBackoff, resetDelta, durationUnit, jitterFunc, timeTeller) } func NewBackoffForTesting(maxBackoff float64, resetDelta float64, durationUnit time.Duration, jitterer JitterFunc, timeTeller TimeTeller) *Backoff { return &Backoff{ step: 0, maxBackoff: maxBackoff, resetDelta: resetDelta, lastAttempt: timeTeller.Now(), durationUnit: durationUnit, jitterer: jitterer, timeTeller: timeTeller, } } func (b *Backoff) maybeReset() { now := b.timeTeller.Now() lastAttemptDelta := float64(now.Sub(b.lastAttempt) / b.durationUnit) b.lastAttempt = now if lastAttemptDelta >= b.resetDelta { b.step = 0 } } func (b *Backoff) GetDelay() time.Duration { b.maybeReset() var synchronizedDuration float64 // Do not add any delay the first time. if b.step == 0 { synchronizedDuration = 0 } else { synchronizedDuration = math.Pow(2, b.step) } if synchronizedDuration < b.maxBackoff { b.step++ } else { synchronizedDuration = b.maxBackoff } duration := time.Duration(b.jitterer(int(synchronizedDuration))) return duration * b.durationUnit } func (b *Backoff) Delay() { b.DelayContext(context.Background()) } func (b *Backoff) DelayContext(ctx context.Context) bool { delay := b.GetDelay() logging.Info("Backoff for %s starts", delay) select { case <-b.timeTeller.After(delay): logging.Info("Backoff for %s ends", delay) case <-ctx.Done(): logging.Info("Backoff for %s canceled by context", delay) return false } return true } alertmanager-irc-relay-0.5.1/backoff_test.go000066400000000000000000000045521475414641600210470ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "testing" "time" ) func FakeJitter(input int) int { return input } func MakeTestingBackoff(maxBackoff float64, resetDelta float64, elapsedTime []int) (*Backoff, *FakeTime) { fakeTime := &FakeTime{ timeseries: elapsedTime, lastIndex: 0, durationUnit: time.Millisecond, afterChan: make(chan time.Time, 1), } backoff := NewBackoffForTesting(maxBackoff, resetDelta, time.Millisecond, FakeJitter, fakeTime) return backoff, fakeTime } func RunBackoffTest(t *testing.T, maxBackoff float64, resetDelta float64, elapsedTime []int, expectedDelays []int) { backoff, _ := MakeTestingBackoff(maxBackoff, resetDelta, elapsedTime) for i, value := range expectedDelays { expected_delay := time.Duration(value) * time.Millisecond delay := backoff.GetDelay() if expected_delay != delay { t.Errorf("Call #%d of GetDelay returned %s (expected %s)", i, delay, expected_delay) } } } func TestBackoffIncreasesAndReachesMax(t *testing.T) { RunBackoffTest(t, 8, 32, // Simple sequential time []int{0, 0, 1, 2, 3, 4, 5, 6, 7}, // Exponential ramp-up to max, then keep max. []int{0, 2, 4, 8, 8, 8, 8, 8}, ) } func TestBackoffReset(t *testing.T) { RunBackoffTest(t, 8, 32, // Simulate two intervals bigger than resetDelta []int{0, 0, 1, 2, 50, 51, 100, 101, 102}, // Delays get reset each time []int{0, 2, 4, 0, 2, 0, 2, 4}, ) } func TestBackoffDelayContext(t *testing.T) { backoff, fakeTime := MakeTestingBackoff(8, 32, []int{0, 0, 0}) ctx, cancel := context.WithCancel(context.Background()) fakeTime.afterChan <- time.Now() if ok := backoff.DelayContext(ctx); !ok { t.Errorf("Expired time does not return true") } cancel() if ok := backoff.DelayContext(ctx); ok { t.Errorf("Canceled context does not return false") } } alertmanager-irc-relay-0.5.1/config.go000066400000000000000000000065101475414641600176560ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "gopkg.in/yaml.v2" "io/ioutil" "os" "github.com/google/alertmanager-irc-relay/logging" ) const ( defaultMsgOnceTemplate = "Alert {{ .GroupLabels.alertname }} for {{ .GroupLabels.job }} is {{ .Status }}" defaultMsgTemplate = "Alert {{ .Labels.alertname }} on {{ .Labels.instance }} is {{ .Status }}" ) type IRCChannel struct { Name string `yaml:"name"` Password string `yaml:"password"` } type Config struct { HTTPHost string `yaml:"http_host"` HTTPPort int `yaml:"http_port"` IRCNick string `yaml:"irc_nickname"` IRCNickPass string `yaml:"irc_nickname_password"` IRCRealName string `yaml:"irc_realname"` IRCHost string `yaml:"irc_host"` IRCPort int `yaml:"irc_port"` IRCHostPass string `yaml:"irc_host_password"` IRCUseSSL bool `yaml:"irc_use_ssl"` IRCVerifySSL bool `yaml:"irc_verify_ssl"` IRCChannels []IRCChannel `yaml:"irc_channels"` MsgTemplate string `yaml:"msg_template"` MsgOnce bool `yaml:"msg_once_per_alert_group"` UsePrivmsg bool `yaml:"use_privmsg"` AlertBufferSize int `yaml:"alert_buffer_size"` NickservName string `yaml:"nickserv_name"` NickservIdentifyPatterns []string `yaml:"nickserv_identify_patterns"` ChanservName string `yaml:"chanserv_name"` } func LoadConfig(configFile string) (*Config, error) { config := &Config{ HTTPHost: "localhost", HTTPPort: 8000, IRCNick: "alertmanager-irc-relay", IRCNickPass: "", IRCRealName: "Alertmanager IRC Relay", IRCHost: "example.com", IRCPort: 7000, IRCHostPass: "", IRCUseSSL: true, IRCVerifySSL: true, IRCChannels: []IRCChannel{}, MsgOnce: false, UsePrivmsg: false, AlertBufferSize: 2048, NickservName: "NickServ", NickservIdentifyPatterns: []string{ "Please choose a different nickname, or identify via", "identify via /msg NickServ identify ", "type /msg NickServ IDENTIFY password", "authenticate yourself to services with the IDENTIFY command", }, ChanservName: "ChanServ", } if configFile != "" { data, err := ioutil.ReadFile(configFile) if err != nil { return nil, err } data = []byte(os.ExpandEnv(string(data))) if err := yaml.Unmarshal(data, config); err != nil { return nil, err } } // Set default template if config does not have one. if config.MsgTemplate == "" { if config.MsgOnce { config.MsgTemplate = defaultMsgOnceTemplate } else { config.MsgTemplate = defaultMsgTemplate } } loadedConfig, _ := yaml.Marshal(config) logging.Debug("Loaded config:\n%s", loadedConfig) return config, nil } alertmanager-irc-relay-0.5.1/config_test.go000066400000000000000000000133551475414641600207220ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io/ioutil" "os" "testing" "gopkg.in/yaml.v2" ) func TestNoConfig(t *testing.T) { noConfigFile := "" config, err := LoadConfig(noConfigFile) if config == nil { t.Errorf("Expected a default config, got: %s", err) } } func TestLoadGoodConfig(t *testing.T) { expectedConfig := &Config{ HTTPHost: "test.web", HTTPPort: 8888, IRCNick: "foo", IRCHost: "irc.example.com", IRCPort: 1234, IRCHostPass: "hostsecret", IRCUseSSL: true, IRCChannels: []IRCChannel{IRCChannel{Name: "#foobar"}}, MsgTemplate: defaultMsgTemplate, MsgOnce: false, UsePrivmsg: false, AlertBufferSize: 666, } expectedData, err := yaml.Marshal(expectedConfig) if err != nil { t.Errorf("Could not serialize test data: %s", err) } tmpfile, err := ioutil.TempFile("", "airtestconfig") if err != nil { t.Errorf("Could not create tmpfile for testing: %s", err) } defer os.Remove(tmpfile.Name()) if _, err := tmpfile.Write(expectedData); err != nil { t.Errorf("Could not write test data in tmpfile: %s", err) } if err := tmpfile.Close(); err != nil { t.Errorf("Could not close tmpfile: %s", err) } config, err := LoadConfig(tmpfile.Name()) if config == nil { t.Errorf("Expected a config, got: %s", err) } configData, err := yaml.Marshal(config) if err != nil { t.Errorf("Could not serialize loaded config") } if string(expectedData) != string(configData) { t.Errorf("Loaded config does not match expected config: %s", configData) } } func TestLoadWithEnvironmentVariables(t *testing.T) { expectedNickPass := "mynickpass" os.Setenv("NICKSERV_PASSWORD", expectedNickPass) defer os.Clearenv() tmpfile, err := ioutil.TempFile("", "airtestenvvarconfig") if err != nil { t.Errorf("Could not create tmpfile for testing: %s", err) } defer os.Remove(tmpfile.Name()) msgOnceConfigData := []byte("irc_nickname_password: $NICKSERV_PASSWORD") if _, err := tmpfile.Write(msgOnceConfigData); err != nil { t.Errorf("Could not write test data in tmpfile: %s", err) } tmpfile.Close() config, err := LoadConfig(tmpfile.Name()) if config == nil { t.Errorf("Expected a config, got: %s", err) } if config.IRCNickPass != expectedNickPass { t.Errorf("Loaded unexpected value: %s (expected: %s)", config.IRCNickPass, expectedNickPass) } } func TestLoadBadFile(t *testing.T) { tmpfile, err := ioutil.TempFile("", "airtestbadfile") if err != nil { t.Errorf("Could not create tmpfile for testing: %s", err) } tmpfile.Close() os.Remove(tmpfile.Name()) config, err := LoadConfig(tmpfile.Name()) if err == nil || config != nil { t.Errorf("Expected no config upon non-existent file. err: %s", err) } } func TestLoadBadConfig(t *testing.T) { tmpfile, err := ioutil.TempFile("", "airtestbadconfig") if err != nil { t.Errorf("Could not create tmpfile for testing: %s", err) } defer os.Remove(tmpfile.Name()) badConfigData := []byte("footest\nbarbaz\n") if _, err := tmpfile.Write(badConfigData); err != nil { t.Errorf("Could not write test data in tmpfile: %s", err) } tmpfile.Close() config, err := LoadConfig(tmpfile.Name()) if err == nil || config != nil { t.Errorf("Expected no config upon bad config. err: %s", err) } } func TestMsgOnceDefaultTemplate(t *testing.T) { tmpfile, err := ioutil.TempFile("", "airtesttemmplateonceconfig") if err != nil { t.Errorf("Could not create tmpfile for testing: %s", err) } defer os.Remove(tmpfile.Name()) msgOnceConfigData := []byte("msg_once_per_alert_group: yes") if _, err := tmpfile.Write(msgOnceConfigData); err != nil { t.Errorf("Could not write test data in tmpfile: %s", err) } tmpfile.Close() config, err := LoadConfig(tmpfile.Name()) if config == nil { t.Errorf("Expected a config, got: %s", err) } if config.MsgTemplate != defaultMsgOnceTemplate { t.Errorf("Expecting defaultMsgOnceTemplate when MsgOnce is true") } } func TestMsgDefaultTemplate(t *testing.T) { tmpfile, err := ioutil.TempFile("", "airtesttemmplateconfig") if err != nil { t.Errorf("Could not create tmpfile for testing: %s", err) } defer os.Remove(tmpfile.Name()) if _, err := tmpfile.Write([]byte("")); err != nil { t.Errorf("Could not write test data in tmpfile: %s", err) } tmpfile.Close() config, err := LoadConfig(tmpfile.Name()) if config == nil { t.Errorf("Expected a config, got: %s", err) } if config.MsgTemplate != defaultMsgTemplate { t.Errorf("Expecting defaultMsgTemplate when MsgOnce is false") } } func TestGivenTemplateNotOverwritten(t *testing.T) { tmpfile, err := ioutil.TempFile("", "airtestexpectedtemmplate") if err != nil { t.Errorf("Could not create tmpfile for testing: %s", err) } defer os.Remove(tmpfile.Name()) expectedTemplate := "Alert {{ .Status }}: {{ .Annotations.SUMMARY }}" configData := []byte(fmt.Sprintf("msg_template: \"%s\"", expectedTemplate)) if _, err := tmpfile.Write(configData); err != nil { t.Errorf("Could not write test data in tmpfile: %s", err) } tmpfile.Close() config, err := LoadConfig(tmpfile.Name()) if config == nil { t.Errorf("Expected a config, got: %s", err) } if config.MsgTemplate != expectedTemplate { t.Errorf("Template does not match configuration") } } alertmanager-irc-relay-0.5.1/context.go000066400000000000000000000024011475414641600200700ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "os" "os/signal" "sync" "github.com/google/alertmanager-irc-relay/logging" ) func WithSignal(ctx context.Context, s ...os.Signal) (context.Context, context.CancelFunc) { sigCtx, cancel := context.WithCancel(ctx) c := make(chan os.Signal, 1) signal.Notify(c, s...) go func() { select { case <-c: logging.Info("Received %s, exiting", s) cancel() case <-ctx.Done(): cancel() } signal.Stop(c) }() return sigCtx, cancel } func WithWaitGroup(ctx context.Context, wg *sync.WaitGroup) context.Context { wgCtx, cancel := context.WithCancel(context.Background()) go func() { <-ctx.Done() wg.Wait() cancel() }() return wgCtx } alertmanager-irc-relay-0.5.1/data.go000066400000000000000000000012131475414641600173150ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main type AlertMsg struct { Channel, Alert string } alertmanager-irc-relay-0.5.1/fake_delayer.go000066400000000000000000000023651475414641600210300ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "time" "github.com/google/alertmanager-irc-relay/logging" ) type FakeDelayerMaker struct{} func (fdm *FakeDelayerMaker) NewDelayer(_ float64, _ float64, _ time.Duration) Delayer { return &FakeDelayer{ DelayOnChan: false, StopDelay: make(chan bool), } } type FakeDelayer struct { DelayOnChan bool StopDelay chan bool } func (f *FakeDelayer) Delay() { f.DelayContext(context.Background()) } func (f *FakeDelayer) DelayContext(ctx context.Context) bool { logging.Info("Faking Backoff") if f.DelayOnChan { logging.Info("Waiting StopDelay signal") <-f.StopDelay logging.Info("Received StopDelay signal") } return true } alertmanager-irc-relay-0.5.1/fake_timeteller.go000066400000000000000000000017671475414641600215560ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "time" ) type FakeTime struct { timeseries []int lastIndex int durationUnit time.Duration afterChan chan time.Time } func (f *FakeTime) Now() time.Time { timeDelta := time.Duration(f.timeseries[f.lastIndex]) * f.durationUnit fakeTime := time.Unix(0, 0).Add(timeDelta) f.lastIndex++ return fakeTime } func (f *FakeTime) After(d time.Duration) <-chan time.Time { return f.afterChan } alertmanager-irc-relay-0.5.1/format.go000066400000000000000000000046201475414641600177010ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bytes" "encoding/json" "net/url" "strings" "text/template" "github.com/google/alertmanager-irc-relay/logging" promtmpl "github.com/prometheus/alertmanager/template" ) type Formatter struct { MsgTemplate *template.Template MsgOnce bool } func NewFormatter(config *Config) (*Formatter, error) { funcMap := template.FuncMap{ "ToUpper": strings.ToUpper, "ToLower": strings.ToLower, "Join": strings.Join, "QueryEscape": url.QueryEscape, "PathEscape": url.PathEscape, } tmpl, err := template.New("msg").Funcs(funcMap).Parse(config.MsgTemplate) if err != nil { return nil, err } return &Formatter{ MsgTemplate: tmpl, MsgOnce: config.MsgOnce, }, nil } func (f *Formatter) FormatMsg(ircChannel string, data interface{}) []string { output := bytes.Buffer{} var msg string if err := f.MsgTemplate.Execute(&output, data); err != nil { msg_bytes, _ := json.Marshal(data) msg = string(msg_bytes) logging.Error("Could not apply msg template on alert (%s): %s", err, msg) logging.Warn("Sending raw alert") alertHandlingErrors.WithLabelValues(ircChannel, "format_msg").Inc() } else { msg = output.String() } // Do not send to IRC messages with newlines, split in multiple messages instead. newLinesSplit := func(r rune) bool { return r == '\n' || r == '\r' } return strings.FieldsFunc(msg, newLinesSplit) } func (f *Formatter) GetMsgsFromAlertMessage(ircChannel string, data *promtmpl.Data) []AlertMsg { msgs := []AlertMsg{} if f.MsgOnce { for _, msg := range f.FormatMsg(ircChannel, data) { msgs = append(msgs, AlertMsg{Channel: ircChannel, Alert: msg}) } } else { for _, alert := range data.Alerts { for _, msg := range f.FormatMsg(ircChannel, alert) { msgs = append(msgs, AlertMsg{Channel: ircChannel, Alert: msg}) } } } return msgs } alertmanager-irc-relay-0.5.1/format_test.go000066400000000000000000000101711475414641600207360ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "encoding/json" "fmt" "reflect" "testing" promtmpl "github.com/prometheus/alertmanager/template" ) func CreateFormatterAndCheckOutput(t *testing.T, c *Config, expected []AlertMsg) { f, _ := NewFormatter(c) var alertMessage = promtmpl.Data{} if err := json.Unmarshal([]byte(testdataSimpleAlertJson), &alertMessage); err != nil { t.Fatal(fmt.Sprintf("Could not unmarshal %s", testdataSimpleAlertJson)) } alertMsgs := f.GetMsgsFromAlertMessage("#somechannel", &alertMessage) if !reflect.DeepEqual(expected, alertMsgs) { t.Error(fmt.Sprintf( "Unexpected alert msg.\nExpected: %s\nActual: %s", expected, alertMsgs)) } } func TestTemplateErrorsCreateRawAlertMsg(t *testing.T) { testingConfig := Config{MsgTemplate: "Bogus template {{ nil }}"} expectedAlertMsgs := []AlertMsg{ AlertMsg{ Channel: "#somechannel", Alert: `{"status":"resolved","labels":{"alertname":"airDown","instance":"instance1:3456","job":"air","service":"prometheus","severity":"ticket","zone":"global"},"annotations":{"DESCRIPTION":"service /prometheus has irc gateway down on instance1","SUMMARY":"service /prometheus air down on instance1"},"startsAt":"2017-05-15T13:49:37.834Z","endsAt":"2017-05-15T13:50:37.835Z","generatorURL":"https://prometheus.example.com/prometheus/...","fingerprint":"66214a361160fb6f"}`, }, AlertMsg{ Channel: "#somechannel", Alert: `{"status":"resolved","labels":{"alertname":"airDown","instance":"instance2:7890","job":"air","service":"prometheus","severity":"ticket","zone":"global"},"annotations":{"DESCRIPTION":"service /prometheus has irc gateway down on instance2","SUMMARY":"service /prometheus air down on instance2"},"startsAt":"2017-05-15T11:47:37.834Z","endsAt":"2017-05-15T11:48:37.834Z","generatorURL":"https://prometheus.example.com/prometheus/...","fingerprint":"25a874c99325d1ce"}`, }, } CreateFormatterAndCheckOutput(t, &testingConfig, expectedAlertMsgs) } func TestAlertsDispatchedOnce(t *testing.T) { testingConfig := Config{ MsgTemplate: "Alert {{ .GroupLabels.alertname }} is {{ .Status }}", MsgOnce: true, } expectedAlertMsgs := []AlertMsg{ AlertMsg{ Channel: "#somechannel", Alert: "Alert airDown is resolved", }, } CreateFormatterAndCheckOutput(t, &testingConfig, expectedAlertMsgs) } func TestStringsFunctions(t *testing.T) { testingConfig := Config{ MsgTemplate: "Alert {{ .GroupLabels.alertname | ToUpper }} is {{ .Status }}", MsgOnce: true, } expectedAlertMsgs := []AlertMsg{ AlertMsg{ Channel: "#somechannel", Alert: "Alert AIRDOWN is resolved", }, } CreateFormatterAndCheckOutput(t, &testingConfig, expectedAlertMsgs) } func TestUrlFunctions(t *testing.T) { testingConfig := Config{ MsgTemplate: "{{ .Annotations.SUMMARY | PathEscape }}", } expectedAlertMsgs := []AlertMsg{ AlertMsg{ Channel: "#somechannel", Alert: "service%20%2Fprometheus%20air%20down%20on%20instance1", }, AlertMsg{ Channel: "#somechannel", Alert: "service%20%2Fprometheus%20air%20down%20on%20instance2", }, } CreateFormatterAndCheckOutput(t, &testingConfig, expectedAlertMsgs) } func TestMultilineTemplates(t *testing.T) { testingConfig := Config{ MsgTemplate: "Alert {{ .GroupLabels.alertname }}\nis\r{{ .Status }}", MsgOnce: true, } expectedAlertMsgs := []AlertMsg{ AlertMsg{ Channel: "#somechannel", Alert: "Alert airDown", }, AlertMsg{ Channel: "#somechannel", Alert: "is", }, AlertMsg{ Channel: "#somechannel", Alert: "resolved", }, } CreateFormatterAndCheckOutput(t, &testingConfig, expectedAlertMsgs) } alertmanager-irc-relay-0.5.1/go.mod000066400000000000000000000003701475414641600171660ustar00rootroot00000000000000module github.com/google/alertmanager-irc-relay go 1.13 require ( github.com/fluffle/goirc v1.1.1 github.com/gorilla/mux v1.8.0 github.com/prometheus/alertmanager v0.21.0 github.com/prometheus/client_golang v1.11.1 gopkg.in/yaml.v2 v2.4.0 ) alertmanager-irc-relay-0.5.1/go.sum000066400000000000000000001667221475414641600172310ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fluffle/goirc v1.1.1 h1:6nO+7rrED3Kp3mngoi9OmQmQHevNwDfjGpYUdWc1s0k= github.com/fluffle/goirc v1.1.1/go.mod h1:iRzPLv2vkuZEtbns5LioYguJkRh/bvshuWg7izf1yeE= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/errors v0.19.4/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/alertmanager v0.21.0 h1:qK51JcUR9l/unhawGA9F9B64OCYfcGewhPNprem/Acc= github.com/prometheus/alertmanager v0.21.0/go.mod h1:h7tJ81NA0VLWvWEayi1QltevFkLF3KxmC/malTcT8Go= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0= github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200513201620-d5fe73897c97 h1:DAuln/hGp+aJiHpID1Y1hYzMEPP5WLwtZHPb50mN0OE= golang.org/x/tools v0.0.0-20200513201620-d5fe73897c97/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= alertmanager-irc-relay-0.5.1/http.go000066400000000000000000000100351475414641600173650ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "encoding/json" "io" "io/ioutil" "net/http" "strconv" "strings" "github.com/google/alertmanager-irc-relay/logging" "github.com/gorilla/mux" promtmpl "github.com/prometheus/alertmanager/template" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( handledAlertGroups = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "webhook_handled_alert_groups", Help: "Number of alert groups received"}, []string{"ircchannel"}, ) handledAlerts = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "webhook_handled_alerts", Help: "Number of single alert messages relayed"}, []string{"ircchannel"}, ) alertHandlingErrors = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "webhook_alert_handling_errors", Help: "Errors while processing webhook requests"}, []string{"ircchannel", "error"}, ) ) type HTTPListener func(string, http.Handler) error type HTTPServer struct { Addr string Port int formatter *Formatter AlertMsgs chan AlertMsg httpListener HTTPListener } func NewHTTPServer(config *Config, alertMsgs chan AlertMsg) ( *HTTPServer, error) { return NewHTTPServerForTesting(config, alertMsgs, http.ListenAndServe) } func NewHTTPServerForTesting(config *Config, alertMsgs chan AlertMsg, httpListener HTTPListener) (*HTTPServer, error) { formatter, err := NewFormatter(config) if err != nil { return nil, err } server := &HTTPServer{ Addr: config.HTTPHost, Port: config.HTTPPort, formatter: formatter, AlertMsgs: alertMsgs, httpListener: httpListener, } return server, nil } func (s *HTTPServer) RelayAlert(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ircChannel := "#" + vars["IRCChannel"] body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1024*1024*1024)) if err != nil { logging.Error("Could not get body: %s", err) alertHandlingErrors.WithLabelValues(ircChannel, "read_body").Inc() return } var alertMessage = promtmpl.Data{} if err := json.Unmarshal(body, &alertMessage); err != nil { logging.Error("Could not decode request body (%s): %s", err, body) alertHandlingErrors.WithLabelValues(ircChannel, "decode_body").Inc() w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(422) // Unprocessable entity if err := json.NewEncoder(w).Encode(err); err != nil { logging.Error("Could not write decoding error: %s", err) return } return } handledAlertGroups.WithLabelValues(ircChannel).Inc() for _, alertMsg := range s.formatter.GetMsgsFromAlertMessage( ircChannel, &alertMessage) { select { case s.AlertMsgs <- alertMsg: handledAlerts.WithLabelValues(ircChannel).Inc() default: logging.Error("Could not send this alert to the IRC routine: %s", alertMsg) alertHandlingErrors.WithLabelValues(ircChannel, "internal_comm_channel_full").Inc() } } } func (s *HTTPServer) Run() { router := mux.NewRouter().StrictSlash(true) router.Path("/metrics").Handler(promhttp.Handler()) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.RelayAlert(w, r) }) router.Path("/{IRCChannel}").Handler(handler).Methods("POST") listenAddr := strings.Join( []string{s.Addr, strconv.Itoa(s.Port)}, ":") logging.Info("Starting HTTP server") if err := s.httpListener(listenAddr, router); err != nil { logging.Error("Could not start http server: %s", err) } } alertmanager-irc-relay-0.5.1/http_test.go000066400000000000000000000075111475414641600204310ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "net/http" "net/http/httptest" "reflect" "strings" "testing" ) type FakeHTTPListener struct { StartedServing chan bool StopServing chan bool AlertMsgs chan AlertMsg // kinda ugly putting it here, but convenient router http.Handler } func (listener *FakeHTTPListener) Serve(_ string, router http.Handler) error { listener.router = router listener.StartedServing <- true <-listener.StopServing return nil } func NewFakeHTTPListener() *FakeHTTPListener { return &FakeHTTPListener{ StartedServing: make(chan bool), StopServing: make(chan bool), AlertMsgs: make(chan AlertMsg, 10), } } func MakeHTTPTestingConfig() *Config { return &Config{ HTTPHost: "test.web", HTTPPort: 8888, MsgTemplate: "Alert {{ .Labels.alertname }} on {{ .Labels.instance }} is {{ .Status }}", } } func RunHTTPTest(t *testing.T, alertData string, url string, testingConfig *Config, listener *FakeHTTPListener) *http.Response { httpServer, err := NewHTTPServerForTesting(testingConfig, listener.AlertMsgs, listener.Serve) if err != nil { t.Fatal(fmt.Sprintf("Could not create HTTP server: %s", err)) } go httpServer.Run() <-listener.StartedServing alertDataReader := strings.NewReader(alertData) request, err := http.NewRequest("POST", url, alertDataReader) if err != nil { t.Fatal(fmt.Sprintf("Could not create HTTP request: %s", err)) } responseRecorder := httptest.NewRecorder() listener.router.ServeHTTP(responseRecorder, request) listener.StopServing <- true return responseRecorder.Result() } func TestAlertsDispatched(t *testing.T) { listener := NewFakeHTTPListener() testingConfig := MakeHTTPTestingConfig() expectedAlertMsgs := []AlertMsg{ AlertMsg{ Channel: "#somechannel", Alert: "Alert airDown on instance1:3456 is resolved", }, AlertMsg{ Channel: "#somechannel", Alert: "Alert airDown on instance2:7890 is resolved", }, } expectedStatusCode := 200 response := RunHTTPTest( t, testdataSimpleAlertJson, "/somechannel", testingConfig, listener) if expectedStatusCode != response.StatusCode { t.Error(fmt.Sprintf("Expected %d status in response, got %d", expectedStatusCode, response.StatusCode)) } for _, expectedAlertMsg := range expectedAlertMsgs { alertMsg := <-listener.AlertMsgs if !reflect.DeepEqual(expectedAlertMsg, alertMsg) { t.Error(fmt.Sprintf( "Unexpected alert msg.\nExpected: %s\nActual: %s", expectedAlertMsg, alertMsg)) } } } func TestRootReturnsError(t *testing.T) { listener := NewFakeHTTPListener() testingConfig := MakeHTTPTestingConfig() expectedStatusCode := 404 response := RunHTTPTest( t, testdataSimpleAlertJson, "/", testingConfig, listener) if expectedStatusCode != response.StatusCode { t.Error(fmt.Sprintf("Expected %d status in response, got %d", expectedStatusCode, response.StatusCode)) } } func TestInvalidDataReturnsError(t *testing.T) { listener := NewFakeHTTPListener() testingConfig := MakeHTTPTestingConfig() expectedStatusCode := 422 response := RunHTTPTest( t, testdataBogusAlertJson, "/somechannel", testingConfig, listener) if expectedStatusCode != response.StatusCode { t.Error(fmt.Sprintf("Expected %d status in response, got %d", expectedStatusCode, response.StatusCode)) } } alertmanager-irc-relay-0.5.1/irc.go000066400000000000000000000227741475414641600172000ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "crypto/tls" "strconv" "strings" "sync" "time" irc "github.com/fluffle/goirc/client" "github.com/google/alertmanager-irc-relay/logging" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) const ( pingFrequencySecs = 60 connectionTimeoutSecs = 30 nickservWaitSecs = 10 ircConnectMaxBackoffSecs = 300 ircConnectBackoffResetSecs = 1800 ) var ( ircConnectedGauge = promauto.NewGauge(prometheus.GaugeOpts{ Name: "irc_connected", Help: "Whether the IRC connection is established", }) ircSentMsgs = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "irc_sent_msgs", Help: "Number of IRC messages sent"}, []string{"ircchannel"}, ) ircSendMsgErrors = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "irc_send_msg_errors", Help: "Errors while sending IRC messages"}, []string{"ircchannel", "error"}, ) ) func loggerHandler(_ *irc.Conn, line *irc.Line) { logging.Info("Received: '%s'", line.Raw) } func makeGOIRCConfig(config *Config) *irc.Config { ircConfig := irc.NewConfig(config.IRCNick) ircConfig.Me.Ident = config.IRCNick ircConfig.Me.Name = config.IRCRealName ircConfig.Server = strings.Join( []string{config.IRCHost, strconv.Itoa(config.IRCPort)}, ":") ircConfig.Pass = config.IRCHostPass ircConfig.SSL = config.IRCUseSSL ircConfig.SSLConfig = &tls.Config{ ServerName: config.IRCHost, InsecureSkipVerify: !config.IRCVerifySSL, } ircConfig.PingFreq = pingFrequencySecs * time.Second ircConfig.Timeout = connectionTimeoutSecs * time.Second ircConfig.NewNick = func(n string) string { return n + "^" } return ircConfig } type IRCNotifier struct { // Nick stores the nickname specified in the config, because irc.Client // might change its copy. Nick string NickPassword string NickservName string NickservIdentifyPatterns []string Client *irc.Conn AlertMsgs chan AlertMsg // irc.Conn has a Connected() method that can tell us wether the TCP // connection is up, and thus if we should trigger connect/disconnect. // We need to track the session establishment also at a higher level to // understand when the server has accepted us and thus when we can join // channels, send notices, etc. sessionUp bool sessionUpSignal chan bool sessionDownSignal chan bool sessionWg sync.WaitGroup channelReconciler *ChannelReconciler UsePrivmsg bool NickservDelayWait time.Duration BackoffCounter Delayer timeTeller TimeTeller } func NewIRCNotifier(config *Config, alertMsgs chan AlertMsg, delayerMaker DelayerMaker, timeTeller TimeTeller) (*IRCNotifier, error) { ircConfig := makeGOIRCConfig(config) client := irc.Client(ircConfig) backoffCounter := delayerMaker.NewDelayer( ircConnectMaxBackoffSecs, ircConnectBackoffResetSecs, time.Second) channelReconciler := NewChannelReconciler(config, client, delayerMaker, timeTeller) notifier := &IRCNotifier{ Nick: config.IRCNick, NickPassword: config.IRCNickPass, NickservName: config.NickservName, NickservIdentifyPatterns: config.NickservIdentifyPatterns, Client: client, AlertMsgs: alertMsgs, sessionUpSignal: make(chan bool), sessionDownSignal: make(chan bool), channelReconciler: channelReconciler, UsePrivmsg: config.UsePrivmsg, NickservDelayWait: nickservWaitSecs * time.Second, BackoffCounter: backoffCounter, timeTeller: timeTeller, } notifier.registerHandlers() return notifier, nil } func (n *IRCNotifier) registerHandlers() { n.Client.HandleFunc(irc.CONNECTED, func(*irc.Conn, *irc.Line) { logging.Info("Session established") n.sessionUpSignal <- true }) n.Client.HandleFunc(irc.DISCONNECTED, func(*irc.Conn, *irc.Line) { logging.Info("Disconnected from IRC") n.sessionDownSignal <- false }) n.Client.HandleFunc(irc.NOTICE, func(_ *irc.Conn, line *irc.Line) { n.HandleNotice(line.Nick, line.Text()) }) for _, event := range []string{"433"} { n.Client.HandleFunc(event, loggerHandler) } } func (n *IRCNotifier) HandleNotice(nick string, msg string) { logging.Info("Received NOTICE from %s: %s", nick, msg) if strings.ToLower(nick) == "nickserv" { n.HandleNickservMsg(msg) } } func (n *IRCNotifier) HandleNickservMsg(msg string) { if n.NickPassword == "" { logging.Debug("Skip processing NickServ request, no password configured") return } // Remove most common formatting options from NickServ messages cleaner := strings.NewReplacer( "\001", "", // bold "\002", "", // faint "\004", "", // underline "\037", "", // underline ) cleanedMsg := cleaner.Replace(msg) for _, identifyPattern := range n.NickservIdentifyPatterns { logging.Debug("Checking if NickServ message matches identify request '%s'", identifyPattern) if strings.Contains(cleanedMsg, identifyPattern) { logging.Info("Handling NickServ request to IDENTIFY") n.Client.Privmsgf(n.NickservName, "IDENTIFY %s", n.NickPassword) return } } } func (n *IRCNotifier) MaybeGhostNick() { if n.NickPassword == "" { logging.Debug("Skip GHOST check, no password configured") return } currentNick := n.Client.Me().Nick if currentNick != n.Nick { logging.Info("My nick is '%s', sending GHOST to NickServ to get '%s'", currentNick, n.Nick) n.Client.Privmsgf(n.NickservName, "GHOST %s %s", n.Nick, n.NickPassword) time.Sleep(n.NickservDelayWait) logging.Info("Changing nick to '%s'", n.Nick) n.Client.Nick(n.Nick) time.Sleep(n.NickservDelayWait) } } func (n *IRCNotifier) MaybeWaitForNickserv() { if n.NickPassword == "" { logging.Debug("Skip NickServ wait, no password configured") return } // Very lazy/optimistic, but this is good enough for my irssi config, // so it should work here as well. logging.Info("Waiting for NickServ to notice us and issue an identify request") time.Sleep(n.NickservDelayWait) } func (n *IRCNotifier) ChannelJoined(ctx context.Context, channel string) bool { isJoined, waitJoined := n.channelReconciler.JoinChannel(channel) if isJoined { return true } select { case <-waitJoined: return true case <-n.timeTeller.After(ircJoinWaitSecs * time.Second): logging.Warn("Channel %s not joined after %d seconds, giving bad news to caller", channel, ircJoinWaitSecs) return false case <-ctx.Done(): logging.Info("Context canceled while waiting for join on channel %s", channel) return false } } func (n *IRCNotifier) SendAlertMsg(ctx context.Context, alertMsg *AlertMsg) { if !n.sessionUp { logging.Error("Cannot send alert to %s : IRC not connected", alertMsg.Channel) ircSendMsgErrors.WithLabelValues(alertMsg.Channel, "not_connected").Inc() return } if !n.ChannelJoined(ctx, alertMsg.Channel) { logging.Error("Cannot send alert to %s : cannot join channel", alertMsg.Channel) ircSendMsgErrors.WithLabelValues(alertMsg.Channel, "not_joined").Inc() return } if n.UsePrivmsg { n.Client.Privmsg(alertMsg.Channel, alertMsg.Alert) } else { n.Client.Notice(alertMsg.Channel, alertMsg.Alert) } ircSentMsgs.WithLabelValues(alertMsg.Channel).Inc() } func (n *IRCNotifier) ShutdownPhase() { if n.sessionUp { logging.Info("IRC client connected, quitting") n.Client.Quit("see ya") logging.Info("Wait for IRC disconnect to complete") select { case <-n.sessionDownSignal: case <-n.timeTeller.After(n.Client.Config().Timeout): logging.Warn("Timeout while waiting for IRC disconnect to complete, stopping anyway") } n.sessionWg.Done() } logging.Info("IRC shutdown complete") } func (n *IRCNotifier) ConnectedPhase(ctx context.Context) { select { case alertMsg := <-n.AlertMsgs: n.SendAlertMsg(ctx, &alertMsg) case <-n.sessionDownSignal: n.sessionUp = false n.sessionWg.Done() n.channelReconciler.Stop() n.Client.Quit("see ya") ircConnectedGauge.Set(0) case <-ctx.Done(): logging.Info("IRC routine asked to terminate") } } func (n *IRCNotifier) SetupPhase(ctx context.Context) { if !n.Client.Connected() { logging.Info("Connecting to IRC %s", n.Client.Config().Server) if ok := n.BackoffCounter.DelayContext(ctx); !ok { return } if err := n.Client.ConnectContext(WithWaitGroup(ctx, &n.sessionWg)); err != nil { logging.Error("Could not connect to IRC: %s", err) return } logging.Info("Connected to IRC server, waiting to establish session") } select { case <-n.sessionUpSignal: n.sessionUp = true n.sessionWg.Add(1) n.MaybeGhostNick() n.MaybeWaitForNickserv() n.channelReconciler.Start(ctx) ircConnectedGauge.Set(1) case <-n.sessionDownSignal: logging.Warn("Receiving a session down before the session is up, this is odd") case <-ctx.Done(): logging.Info("IRC routine asked to terminate") } } func (n *IRCNotifier) Run(ctx context.Context, stopWg *sync.WaitGroup) { defer stopWg.Done() for ctx.Err() != context.Canceled { if !n.sessionUp { n.SetupPhase(ctx) } else { n.ConnectedPhase(ctx) } } n.ShutdownPhase() } alertmanager-irc-relay-0.5.1/irc_server_for_test.go000066400000000000000000000121421475414641600224570ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bufio" "errors" "fmt" "io" "net" "strings" "sync" "testing" irc "github.com/fluffle/goirc/client" "github.com/google/alertmanager-irc-relay/logging" ) type LineHandlerFunc func(*bufio.ReadWriter, *irc.Line) error func hJOIN(conn *bufio.ReadWriter, line *irc.Line) error { r := fmt.Sprintf(":foo!foo@example.com JOIN :%s\n", line.Args[0]) _, err := conn.WriteString(r) return err } func hUSER(conn *bufio.ReadWriter, line *irc.Line) error { r := fmt.Sprintf(":example.com 001 %s :Welcome\n", line.Args[0]) _, err := conn.WriteString(r) return err } func hQUIT(conn *bufio.ReadWriter, line *irc.Line) error { return fmt.Errorf("client asked to terminate") } type closeEarlyHandler func() type testServer struct { net.Listener Client net.Conn ServingWaitGroup sync.WaitGroup ConnectionsWaitGroup sync.WaitGroup lineHandlersMu sync.Mutex lineHandlers map[string]LineHandlerFunc Log []string closeEarlyMu sync.Mutex closeEarlyHandler } func (s *testServer) setDefaultHandlers() { if s.lineHandlers == nil { s.lineHandlers = make(map[string]LineHandlerFunc) } s.lineHandlers["JOIN"] = hJOIN s.lineHandlers["USER"] = hUSER s.lineHandlers["QUIT"] = hQUIT } func (s *testServer) getHandler(cmd string) LineHandlerFunc { s.lineHandlersMu.Lock() defer s.lineHandlersMu.Unlock() return s.lineHandlers[cmd] } func (s *testServer) SetHandler(cmd string, h LineHandlerFunc) { s.lineHandlersMu.Lock() defer s.lineHandlersMu.Unlock() if h == nil { delete(s.lineHandlers, cmd) } else { s.lineHandlers[cmd] = h } } func (s *testServer) handleLine(conn *bufio.ReadWriter, line *irc.Line) error { s.Log = append(s.Log, strings.Trim(line.Raw, " \r\n")) handler := s.getHandler(line.Cmd) if handler == nil { logging.Info("=Server= No handler for command '%s', skipping", line.Cmd) return nil } return handler(conn, line) } func (s *testServer) handleConnection(conn net.Conn) { defer func() { s.Client = nil conn.Close() s.ConnectionsWaitGroup.Done() }() bufConn := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) for { msg, err := bufConn.ReadBytes('\n') if err != nil { if err == io.EOF { logging.Info("=Server= Client %s disconnected", conn.RemoteAddr().String()) } else { logging.Info("=Server= Could not read from %s: %s", conn.RemoteAddr().String(), err) } return } logging.Info("=Server= Received %s", msg) line := irc.ParseLine(string(msg)) if line == nil { logging.Error("=Server= Could not parse received line") continue } err = s.handleLine(bufConn, line) if err != nil { logging.Info("=Server= Closing connection: %s", err) return } bufConn.Flush() } } func (s *testServer) SendMsg(msg string) error { if s.Client == nil { return errors.New("Cannot write without client connected") } bufConn := bufio.NewWriter(s.Client) logging.Info("=Server= sending to client: %s", msg) _, err := bufConn.WriteString(msg) bufConn.Flush() return err } func (s *testServer) SetCloseEarly(h closeEarlyHandler) { s.closeEarlyMu.Lock() defer s.closeEarlyMu.Unlock() s.closeEarlyHandler = h } func (s *testServer) handleCloseEarly(conn net.Conn) bool { s.closeEarlyMu.Lock() defer s.closeEarlyMu.Unlock() if s.closeEarlyHandler == nil { return false } logging.Info("=Server= Closing connection early") conn.Close() s.closeEarlyHandler() return true } func (s *testServer) Serve() { defer s.ServingWaitGroup.Done() for { conn, err := s.Listener.Accept() if err != nil { logging.Info("=Server= Stopped accepting new connections") return } logging.Info("=Server= New client connected from %s", conn.RemoteAddr().String()) if s.handleCloseEarly(conn) { continue } s.Client = conn s.ConnectionsWaitGroup.Add(1) s.handleConnection(conn) } } func (s *testServer) Stop() { s.Listener.Close() s.ServingWaitGroup.Wait() s.ConnectionsWaitGroup.Wait() } func makeTestServer(t *testing.T) (*testServer, int) { server := new(testServer) server.Log = make([]string, 0) server.setDefaultHandlers() addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("=Server= Could not resolve tcp addr: %s", err) } listener, err := net.ListenTCP("tcp", addr) if err != nil { t.Fatalf("=Server= Could not create listener: %s", err) } addr = listener.Addr().(*net.TCPAddr) logging.Info("=Server= Test server listening on %s", addr.String()) server.Listener = listener server.ServingWaitGroup.Add(1) go func() { server.Serve() }() addr = listener.Addr().(*net.TCPAddr) return server, addr.Port } alertmanager-irc-relay-0.5.1/irc_test.go000066400000000000000000000353551475414641600202360ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bufio" "context" "fmt" "reflect" "strings" "sync" "testing" "time" irc "github.com/fluffle/goirc/client" "github.com/google/alertmanager-irc-relay/logging" ) func makeTestIRCConfig(IRCPort int) *Config { return &Config{ IRCNick: "foo", IRCNickPass: "", IRCHost: "127.0.0.1", IRCPort: IRCPort, IRCUseSSL: false, IRCChannels: []IRCChannel{ IRCChannel{Name: "#foo"}, }, UsePrivmsg: false, NickservIdentifyPatterns: []string{ "identify yourself ktnxbye", }, NickservName: "NickServ", ChanservName: "ChanServ", } } func makeTestNotifier(t *testing.T, config *Config) (*IRCNotifier, chan AlertMsg, context.Context, context.CancelFunc, *sync.WaitGroup) { fakeDelayerMaker := &FakeDelayerMaker{} fakeTime := &FakeTime{ afterChan: make(chan time.Time, 1), } alertMsgs := make(chan AlertMsg) ctx, cancel := context.WithCancel(context.Background()) stopWg := sync.WaitGroup{} stopWg.Add(1) notifier, err := NewIRCNotifier(config, alertMsgs, fakeDelayerMaker, fakeTime) if err != nil { t.Fatal(fmt.Sprintf("Could not create IRC notifier: %s", err)) } notifier.Client.Config().Flood = true return notifier, alertMsgs, ctx, cancel, &stopWg } func TestServerPassword(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) config.IRCHostPass = "hostsecret" notifier, _, ctx, cancel, stopWg := makeTestNotifier(t, config) var testStep sync.WaitGroup joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return nil } server.SetHandler("JOIN", joinHandler) testStep.Add(1) go notifier.Run(ctx, stopWg) testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "PASS hostsecret", "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Did not send IRC server password. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestSendAlertOnPreJoinedChannel(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) notifier, alertMsgs, ctx, cancel, stopWg := makeTestNotifier(t, config) var testStep sync.WaitGroup testChannel := "#foo" testMessage := "test message" // Send the alert after configured channels have joined, to ensure we // check for no re-join attempt. joinedHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { if line.Args[0] == testChannel { testStep.Done() } return hJOIN(conn, line) } server.SetHandler("JOIN", joinedHandler) testStep.Add(1) go notifier.Run(ctx, stopWg) testStep.Wait() server.SetHandler("JOIN", hJOIN) noticeHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return nil } server.SetHandler("NOTICE", noticeHandler) testStep.Add(1) alertMsgs <- AlertMsg{Channel: testChannel, Alert: testMessage} testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "NOTICE #foo :test message", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Alert not sent correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestUsePrivmsgToSendAlertOnPreJoinedChannel(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) config.UsePrivmsg = true notifier, alertMsgs, ctx, cancel, stopWg := makeTestNotifier(t, config) var testStep sync.WaitGroup testChannel := "#foo" testMessage := "test message" // Send the alert after configured channels have joined, to ensure we // check for no re-join attempt. joinedHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { if line.Args[0] == testChannel { testStep.Done() } return hJOIN(conn, line) } server.SetHandler("JOIN", joinedHandler) testStep.Add(1) go notifier.Run(ctx, stopWg) testStep.Wait() server.SetHandler("JOIN", hJOIN) privmsgHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return nil } server.SetHandler("PRIVMSG", privmsgHandler) testStep.Add(1) alertMsgs <- AlertMsg{Channel: testChannel, Alert: testMessage} testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "PRIVMSG #foo :test message", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Alert not sent correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestSendAlertAndJoinChannel(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) notifier, alertMsgs, ctx, cancel, stopWg := makeTestNotifier(t, config) var testStep sync.WaitGroup testChannel := "#foobar" testMessage := "test message" // Send the alert after configured channels have joined, to ensure log // ordering. joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return hJOIN(conn, line) } server.SetHandler("JOIN", joinHandler) testStep.Add(1) go notifier.Run(ctx, stopWg) testStep.Wait() server.SetHandler("JOIN", hJOIN) noticeHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return nil } server.SetHandler("NOTICE", noticeHandler) testStep.Add(1) alertMsgs <- AlertMsg{Channel: testChannel, Alert: testMessage} testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", // #foobar joined before sending message "PRIVMSG ChanServ :UNBAN #foobar", "JOIN #foobar", "NOTICE #foobar :test message", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Alert not sent correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestSendAlertDisconnected(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) notifier, alertMsgs, ctx, cancel, stopWg := makeTestNotifier(t, config) var testStep, holdUserStep sync.WaitGroup testChannel := "#foo" disconnectedTestMessage := "disconnected test message" connectedTestMessage := "connected test message" // First send an alert while the session is not established. testStep.Add(1) holdUserStep.Add(1) holdUser := func(conn *bufio.ReadWriter, line *irc.Line) error { logging.Info("=Server= Wait before completing session") testStep.Wait() logging.Info("=Server= Completing session") holdUserStep.Done() return hUSER(conn, line) } server.SetHandler("USER", holdUser) go notifier.Run(ctx, stopWg) // Alert channels is not consumed while disconnected select { case alertMsgs <- AlertMsg{Channel: testChannel, Alert: disconnectedTestMessage}: t.Error("Alert consumed while disconnected") default: } testStep.Done() holdUserStep.Wait() // Make sure session is established by checking that pre-joined // channel is there. testStep.Add(1) joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return hJOIN(conn, line) } server.SetHandler("JOIN", joinHandler) testStep.Wait() // Now send and wait until a notice has been received. testStep.Add(1) noticeHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return nil } server.SetHandler("NOTICE", noticeHandler) alertMsgs <- AlertMsg{Channel: testChannel, Alert: connectedTestMessage} testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", // Only message sent while being connected is received. "NOTICE #foo :connected test message", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Alert not sent correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestReconnect(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) notifier, _, ctx, cancel, stopWg := makeTestNotifier(t, config) var testStep sync.WaitGroup joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return hJOIN(conn, line) } server.SetHandler("JOIN", joinHandler) testStep.Add(1) go notifier.Run(ctx, stopWg) // Wait until the pre-joined channel is seen. testStep.Wait() // Simulate disconnection. testStep.Add(1) server.Client.Close() // Wait again until the pre-joined channel is seen. testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ // Commands from first connection "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", // Commands from reconnection "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Reconnection did not happen correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestConnectErrorRetry(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) // Attempt SSL handshake. The server does not support it, resulting in // a connection error. config.IRCUseSSL = true notifier, _, ctx, cancel, stopWg := makeTestNotifier(t, config) // Pilot reconnect attempts via backoff delay to prevent race // conditions in the test while we change the components behavior on // the fly. delayer := notifier.BackoffCounter.(*FakeDelayer) delayer.DelayOnChan = true var testStep, joinStep sync.WaitGroup testStep.Add(1) earlyHandler := func() { testStep.Done() } server.SetCloseEarly(earlyHandler) go notifier.Run(ctx, stopWg) delayer.StopDelay <- true testStep.Wait() // We have caused a connection failure, now check for a reconnection notifier.Client.Config().SSL = false joinStep.Add(1) joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { joinStep.Done() return hJOIN(conn, line) } server.SetHandler("JOIN", joinHandler) server.SetCloseEarly(nil) delayer.StopDelay <- true joinStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Reconnection did not happen correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestIdentify(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) config.IRCNickPass = "nickpassword" notifier, _, ctx, cancel, stopWg := makeTestNotifier(t, config) notifier.NickservDelayWait = 0 * time.Second var testStep sync.WaitGroup // Trigger NickServ identify request when we see the NICK command // Note: We also test formatting cleanup with this message nickHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { var err error _, err = conn.WriteString(":NickServ!NickServ@services. NOTICE airtest :This nickname is registered. Please choose a different nickname, or \002identify yourself\002 ktnxbye.\n") return err } server.SetHandler("NICK", nickHandler) // Wait until the pre-joined channel is seen (joining happens // after identification). joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return hJOIN(conn, line) } server.SetHandler("JOIN", joinHandler) testStep.Add(1) go notifier.Run(ctx, stopWg) testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", "PRIVMSG NickServ :IDENTIFY nickpassword", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Identification did not happen correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestGhost(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) config.IRCNickPass = "nickpassword" notifier, _, ctx, cancel, stopWg := makeTestNotifier(t, config) notifier.NickservDelayWait = 0 * time.Second var testStep sync.WaitGroup // Trigger 433 for first nick when we see the USER command userHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { var err error if line.Args[0] == "foo" { _, err = conn.WriteString(":example.com 433 * foo :nick in use\n") } return err } server.SetHandler("USER", userHandler) // Trigger 001 when we see NICK foo^ nickHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { var err error if line.Args[0] == "foo^" { _, err = conn.WriteString(":example.com 001 foo^ :Welcome\n") } return err } server.SetHandler("NICK", nickHandler) // Wait until the pre-joined channel is seen (joining happens // after ghosting). joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return hJOIN(conn, line) } server.SetHandler("JOIN", joinHandler) testStep.Add(1) go notifier.Run(ctx, stopWg) testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", "NICK foo^", "PRIVMSG NickServ :GHOST foo nickpassword", "NICK foo", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "QUIT :see ya", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Ghosting did not happen correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } func TestStopRunningWhenHalfConnected(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) notifier, _, ctx, cancel, stopWg := makeTestNotifier(t, config) var testStep sync.WaitGroup // Send a StopRunning request while the client is connected but the // session is not up testStep.Add(1) holdUser := func(conn *bufio.ReadWriter, line *irc.Line) error { logging.Info("=Server= NOT completing session") testStep.Done() return nil } server.SetHandler("USER", holdUser) go notifier.Run(ctx, stopWg) testStep.Wait() cancel() stopWg.Wait() server.Stop() expectedCommands := []string{ "NICK foo", "USER foo 12 * :", } if !reflect.DeepEqual(expectedCommands, server.Log) { t.Error("Alert not sent correctly. Received commands:\n", strings.Join(server.Log, "\n")) } } alertmanager-irc-relay-0.5.1/logging/000077500000000000000000000000001475414641600175065ustar00rootroot00000000000000alertmanager-irc-relay-0.5.1/logging/logging.go000066400000000000000000000037001475414641600214630ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package logging import ( "flag" "fmt" "log" "os" goirc_logging "github.com/fluffle/goirc/logging" ) const ( loggingCallDepth = 3 ) type Logger interface { Debug(format string, args ...interface{}) Info(format string, args ...interface{}) Warn(format string, args ...interface{}) Error(format string, args ...interface{}) } var logger Logger type stdOutLogger struct { out *log.Logger } var debugFlag = flag.Bool("debug", false, "Enable debug logging.") func (l stdOutLogger) Debug(f string, a ...interface{}) { if *debugFlag { l.out.Output(loggingCallDepth, fmt.Sprintf("DEBUG "+f, a...)) } } func (l stdOutLogger) Info(f string, a ...interface{}) { l.out.Output(loggingCallDepth, fmt.Sprintf("INFO "+f, a...)) } func (l stdOutLogger) Warn(f string, a ...interface{}) { l.out.Output(loggingCallDepth, fmt.Sprintf("WARN "+f, a...)) } func (l stdOutLogger) Error(f string, a ...interface{}) { l.out.Output(loggingCallDepth, fmt.Sprintf("ERROR "+f, a...)) } func init() { logger = stdOutLogger{ out: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds|log.Lshortfile), } goirc_logging.SetLogger(logger) } func Debug(f string, a ...interface{}) { logger.Debug(f, a...) } func Info(f string, a ...interface{}) { logger.Info(f, a...) } func Warn(f string, a ...interface{}) { logger.Warn(f, a...) } func Error(f string, a ...interface{}) { logger.Error(f, a...) } alertmanager-irc-relay-0.5.1/main.go000066400000000000000000000027141475414641600173370ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "flag" "sync" "syscall" "github.com/google/alertmanager-irc-relay/logging" ) func main() { configFile := flag.String("config", "", "Config file path.") flag.Parse() ctx, _ := WithSignal(context.Background(), syscall.SIGINT, syscall.SIGTERM) stopWg := sync.WaitGroup{} config, err := LoadConfig(*configFile) if err != nil { logging.Error("Could not load config: %s", err) return } alertMsgs := make(chan AlertMsg, config.AlertBufferSize) stopWg.Add(1) ircNotifier, err := NewIRCNotifier(config, alertMsgs, &BackoffMaker{}, &RealTime{}) if err != nil { logging.Error("Could not create IRC notifier: %s", err) return } go ircNotifier.Run(ctx, &stopWg) httpServer, err := NewHTTPServer(config, alertMsgs) if err != nil { logging.Error("Could not create HTTP server: %s", err) return } go httpServer.Run() stopWg.Wait() } alertmanager-irc-relay-0.5.1/reconciler.go000066400000000000000000000153151475414641600205410ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "sync" "time" irc "github.com/fluffle/goirc/client" "github.com/google/alertmanager-irc-relay/logging" ) const ( ircJoinWaitSecs = 10 ircJoinMaxBackoffSecs = 300 ircJoinBackoffResetSecs = 1800 ) type channelState struct { channel IRCChannel chanservName string client *irc.Conn delayer Delayer timeTeller TimeTeller joinDone chan struct{} // joined when channel is closed joined bool joinUnsetSignal chan bool mu sync.Mutex } func newChannelState(channel *IRCChannel, client *irc.Conn, delayerMaker DelayerMaker, timeTeller TimeTeller, chanservName string) *channelState { delayer := delayerMaker.NewDelayer(ircJoinMaxBackoffSecs, ircJoinBackoffResetSecs, time.Second) return &channelState{ channel: *channel, client: client, delayer: delayer, timeTeller: timeTeller, joinDone: make(chan struct{}), joined: false, joinUnsetSignal: make(chan bool), chanservName: chanservName, } } func (c *channelState) JoinDone() <-chan struct{} { c.mu.Lock() defer c.mu.Unlock() return c.joinDone } func (c *channelState) SetJoined() { c.mu.Lock() defer c.mu.Unlock() if c.joined == true { logging.Warn("Not setting JOIN state on channel %s: already set", c.channel.Name) return } logging.Info("Setting JOIN state on channel %s", c.channel.Name) c.joined = true close(c.joinDone) } func (c *channelState) UnsetJoined() { c.mu.Lock() defer c.mu.Unlock() if c.joined == false { logging.Warn("Not removing JOIN state on channel %s: already not set", c.channel.Name) return } logging.Info("Removing JOIN state on channel %s", c.channel.Name) c.joined = false c.joinDone = make(chan struct{}) // eventually poke monitor routine select { case c.joinUnsetSignal <- true: default: } } func (c *channelState) join(ctx context.Context) { logging.Info("Channel %s monitor: waiting to join", c.channel.Name) if ok := c.delayer.DelayContext(ctx); !ok { return } // Try to unban ourselves, just in case c.client.Privmsgf(c.chanservName, "UNBAN %s", c.channel.Name) c.client.Join(c.channel.Name, c.channel.Password) logging.Info("Channel %s monitor: join request sent", c.channel.Name) select { case <-c.JoinDone(): logging.Info("Channel %s monitor: join succeeded", c.channel.Name) case <-c.timeTeller.After(ircJoinWaitSecs * time.Second): logging.Warn("Channel %s monitor: could not join after %d seconds, will retry", c.channel.Name, ircJoinWaitSecs) case <-ctx.Done(): logging.Info("Channel %s monitor: context canceled while waiting for join", c.channel.Name) } } func (c *channelState) monitorJoinUnset(ctx context.Context) { select { case <-c.joinUnsetSignal: logging.Info("Channel %s monitor: channel no longer joined", c.channel.Name) case <-ctx.Done(): logging.Info("Channel %s monitor: context canceled while monitoring", c.channel.Name) } } func (c *channelState) Monitor(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() joined := func() bool { c.mu.Lock() defer c.mu.Unlock() return c.joined } for ctx.Err() != context.Canceled { if !joined() { c.join(ctx) } else { c.monitorJoinUnset(ctx) } } } type ChannelReconciler struct { preJoinChannels []IRCChannel client *irc.Conn delayerMaker DelayerMaker timeTeller TimeTeller channels map[string]*channelState chanservName string stopCtx context.Context stopCtxCancel context.CancelFunc stopWg sync.WaitGroup mu sync.Mutex } func NewChannelReconciler(config *Config, client *irc.Conn, delayerMaker DelayerMaker, timeTeller TimeTeller) *ChannelReconciler { reconciler := &ChannelReconciler{ preJoinChannels: config.IRCChannels, client: client, delayerMaker: delayerMaker, timeTeller: timeTeller, channels: make(map[string]*channelState), chanservName: config.ChanservName, } reconciler.registerHandlers() return reconciler } func (r *ChannelReconciler) registerHandlers() { r.client.HandleFunc(irc.JOIN, func(_ *irc.Conn, line *irc.Line) { r.HandleJoin(line.Nick, line.Args[0]) }) r.client.HandleFunc(irc.KICK, func(_ *irc.Conn, line *irc.Line) { r.HandleKick(line.Args[1], line.Args[0]) }) } func (r *ChannelReconciler) HandleJoin(nick string, channel string) { r.mu.Lock() defer r.mu.Unlock() if nick != r.client.Me().Nick { // received join info for somebody else return } logging.Info("Received JOIN confirmation for channel %s", channel) c, ok := r.channels[channel] if !ok { logging.Warn("Not processing JOIN for channel %s: unknown channel", channel) return } c.SetJoined() } func (r *ChannelReconciler) HandleKick(nick string, channel string) { r.mu.Lock() defer r.mu.Unlock() if nick != r.client.Me().Nick { // received kick info for somebody else return } logging.Info("Received KICK for channel %s", channel) c, ok := r.channels[channel] if !ok { logging.Warn("Not processing KICK for channel %s: unknown channel", channel) return } c.UnsetJoined() } func (r *ChannelReconciler) unsafeAddChannel(channel *IRCChannel) *channelState { c := newChannelState(channel, r.client, r.delayerMaker, r.timeTeller, r.chanservName) r.stopWg.Add(1) go c.Monitor(r.stopCtx, &r.stopWg) r.channels[channel.Name] = c return c } func (r *ChannelReconciler) JoinChannel(channel string) (bool, <-chan struct{}) { r.mu.Lock() defer r.mu.Unlock() c, ok := r.channels[channel] if !ok { logging.Info("Request to JOIN new channel %s", channel) c = r.unsafeAddChannel(&IRCChannel{Name: channel}) } select { case <-c.JoinDone(): return true, nil default: return false, c.JoinDone() } } func (r *ChannelReconciler) unsafeStop() { if r.stopCtxCancel == nil { // calling stop before first start, ignoring return } r.stopCtxCancel() r.stopWg.Wait() r.channels = make(map[string]*channelState) } func (r *ChannelReconciler) Stop() { r.mu.Lock() defer r.mu.Unlock() r.unsafeStop() } func (r *ChannelReconciler) Start(ctx context.Context) { r.mu.Lock() defer r.mu.Unlock() r.unsafeStop() r.stopCtx, r.stopCtxCancel = context.WithCancel(ctx) for _, channel := range r.preJoinChannels { r.unsafeAddChannel(&channel) } } alertmanager-irc-relay-0.5.1/reconciler_test.go000066400000000000000000000077351475414641600216070ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bufio" "context" "reflect" "sort" "sync" "testing" "time" irc "github.com/fluffle/goirc/client" ) func makeTestReconciler(config *Config) (*ChannelReconciler, chan bool, chan bool, *FakeTime) { sessionUp := make(chan bool) sessionDown := make(chan bool) client := irc.Client(makeGOIRCConfig(config)) client.Config().Flood = true client.HandleFunc(irc.CONNECTED, func(*irc.Conn, *irc.Line) { sessionUp <- true }) client.HandleFunc(irc.DISCONNECTED, func(*irc.Conn, *irc.Line) { sessionDown <- false }) fakeDelayerMaker := &FakeDelayerMaker{} fakeTime := &FakeTime{ afterChan: make(chan time.Time, 1), } reconciler := NewChannelReconciler(config, client, fakeDelayerMaker, fakeTime) return reconciler, sessionUp, sessionDown, fakeTime } func TestPreJoinChannels(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) config.IRCChannels = []IRCChannel{ IRCChannel{Name: "#foo"}, IRCChannel{Name: "#bar"}, IRCChannel{Name: "#baz"}, } reconciler, sessionUp, sessionDown, _ := makeTestReconciler(config) var testStep sync.WaitGroup joinedChannels := []string{} joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { joinedChannels = append(joinedChannels, line.Args[0]) if len(joinedChannels) == 3 { testStep.Done() } return hJOIN(conn, line) } server.SetHandler("JOIN", joinHandler) testStep.Add(1) reconciler.client.Connect() <-sessionUp reconciler.Start(context.Background()) testStep.Wait() reconciler.client.Quit("see ya") <-sessionDown reconciler.Stop() server.Stop() expectedJoinedChannels := []string{"#bar", "#baz", "#foo"} sort.Strings(joinedChannels) if !reflect.DeepEqual(expectedJoinedChannels, joinedChannels) { t.Error("Did not pre-join channels") } } func TestKeepJoining(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) reconciler, sessionUp, sessionDown, fakeTime := makeTestReconciler(config) var testStep sync.WaitGroup var joinedCounter int // Confirm join only after a few attempts joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { joinedCounter++ if joinedCounter == 3 { testStep.Done() return hJOIN(conn, line) } else { fakeTime.afterChan <- time.Now() } return nil } server.SetHandler("JOIN", joinHandler) testStep.Add(1) reconciler.client.Connect() <-sessionUp reconciler.Start(context.Background()) testStep.Wait() reconciler.client.Quit("see ya") <-sessionDown reconciler.Stop() server.Stop() expectedJoinedCounter := 3 if !reflect.DeepEqual(expectedJoinedCounter, joinedCounter) { t.Error("Did not keep joining") } } func TestKickRejoin(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) reconciler, sessionUp, sessionDown, _ := makeTestReconciler(config) var testStep sync.WaitGroup // Wait for channel to be joined joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { hJOIN(conn, line) testStep.Done() return nil } server.SetHandler("JOIN", joinHandler) testStep.Add(1) reconciler.client.Connect() <-sessionUp reconciler.Start(context.Background()) testStep.Wait() // Kick and wait for channel to be joined again testStep.Add(1) server.SendMsg(":test!~test@example.com KICK #foo foo :Bye!\n") testStep.Wait() reconciler.client.Quit("see ya") <-sessionDown reconciler.Stop() server.Stop() } alertmanager-irc-relay-0.5.1/testdata.go000066400000000000000000000051161475414641600202230ustar00rootroot00000000000000// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main const ( testdataSimpleAlertJson = ` { "status": "resolved", "receiver": "example_receiver", "groupLabels": { "alertname": "airDown", "service": "prometheus" }, "commonLabels": { "alertname": "airDown", "job": "air", "service": "prometheus", "severity": "ticket", "zone": "global" }, "commonAnnotations": {}, "externalURL": "https://prometheus.example.com/alertmanager", "alerts": [ { "annotations": { "SUMMARY": "service /prometheus air down on instance1", "DESCRIPTION": "service /prometheus has irc gateway down on instance1" }, "endsAt": "2017-05-15T13:50:37.835Z", "generatorURL": "https://prometheus.example.com/prometheus/...", "fingerprint": "66214a361160fb6f", "labels": { "alertname": "airDown", "instance": "instance1:3456", "job": "air", "service": "prometheus", "severity": "ticket", "zone": "global" }, "startsAt": "2017-05-15T13:49:37.834Z", "status": "resolved" }, { "annotations": { "SUMMARY": "service /prometheus air down on instance2", "DESCRIPTION": "service /prometheus has irc gateway down on instance2" }, "endsAt": "2017-05-15T11:48:37.834Z", "generatorURL": "https://prometheus.example.com/prometheus/...", "fingerprint": "25a874c99325d1ce", "labels": { "alertname": "airDown", "instance": "instance2:7890", "job": "air", "service": "prometheus", "severity": "ticket", "zone": "global" }, "startsAt": "2017-05-15T11:47:37.834Z", "status": "resolved" } ] } ` testdataBogusAlertJson = `{"this is not": "a valid alert",}` ) alertmanager-irc-relay-0.5.1/time.go000066400000000000000000000016631475414641600173530ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "time" ) // TimeTeller interface allows injection of fake time during testing type TimeTeller interface { Now() time.Time After(time.Duration) <-chan time.Time } type RealTime struct{} func (r *RealTime) Now() time.Time { return time.Now() } func (r *RealTime) After(d time.Duration) <-chan time.Time { return time.After(d) }