From 03827874cfc51b4be6709459d5263ef87f31dbe4 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 9 Jan 2021 15:49:19 +0100 Subject: [PATCH] Configure golangci-lint and apply suggestions --- .golangci.yml | 63 ++++++ boardvoting.go | 329 +++++++++++++++++++++------- boardvoting/main.go | 60 ++++-- db/migrations_embedded.go | 23 +- forms.go | 85 +++++--- go.mod | 29 +-- go.sum | 369 ++++++++++++++++++++++++++++++++ jobs.go | 87 +++++--- models.go | 439 ++++++++++++++++++++++---------------- notifications.go | 90 +++++--- 10 files changed, 1185 insertions(+), 389 deletions(-) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8e3aa16 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,63 @@ +--- +run: + skip-files: + - boardvoting/assets.go + +output: + sort-results: true + +linter-settings: + goheader: + values: + const: + ORGANIZATION: CAcert Inc. + template: | + Copyright {{ YEAR-RANGE }} {{ ORGANIZATION }} + SPDX-License-Identifier: Apache-2.0 + + 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. + goimports: + local-prefixes: git.cacert.org/cacert-boardvoting + misspell: + locale: US + ignore-words: + - CAcert + +linters: + disable-all: false + enable: + - bodyclose + - errorlint + - gocognit + - goconst + - gocritic + - gofmt + - goheader + - goimports + - golint + - gomnd + - gosec + - interfacer + - lll + - makezero + - misspell + - nakedret + - nestif + - nlreturn + - nolintlint + - predeclared + - rowserrcheck + - scopelint + - sqlclosecheck + - wrapcheck + - wsl diff --git a/boardvoting.go b/boardvoting.go index dca1985..f1d9933 100644 --- a/boardvoting.go +++ b/boardvoting.go @@ -1,18 +1,20 @@ /* - Copyright 2017-2020 Jan Dittberner +Copyright 2017-2021 Jan Dittberner - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this program except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this program except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. +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. */ + +// The CAcert board voting software. package main import ( @@ -51,12 +53,21 @@ var csrfKey []byte var version = "undefined" var build = "undefined" -const sessionCookieName = "votesession" +const ( + cookieSecretMinLen = 32 + csrfKeyLength = 32 + httpIdleTimeout = 5 + httpReadHeaderTimeout = 10 + httpReadTimeout = 10 + httpWriteTimeout = 60 + sessionCookieName = "votesession" +) func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) { funcMaps := sprig.FuncMap() funcMaps["nl2br"] = func(text string) template.HTML { - return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "
", -1)) + // #nosec G203 input is sanitized + return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "
")) } funcMaps[csrf.TemplateTag] = func() template.HTML { return csrf.TemplateField(r) @@ -65,20 +76,24 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, var baseTemplate *template.Template for count, t := range templates { - var err error - var assetBytes []byte - if //noinspection GoUnresolvedReference - assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil { + var ( + err error + assetBytes []byte + ) + + if assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) + + return + } + + if count == 0 { + if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } } else { - if count == 0 { - if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } } } @@ -100,27 +115,35 @@ const ( func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) { emailsTried := make(map[string]bool) + for _, cert := range r.TLS.PeerCertificates { for _, extKeyUsage := range cert.ExtKeyUsage { - if extKeyUsage == x509.ExtKeyUsageClientAuth { - for _, emailAddress := range cert.EmailAddresses { - emailLower := strings.ToLower(emailAddress) - emailsTried[emailLower] = true - voter, err := FindVoterByAddress(emailLower) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if voter != nil { - requestContext := context.WithValue(r.Context(), ctxVoter, voter) - requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert) - handler(w, r.WithContext(requestContext)) - return - } + if extKeyUsage != x509.ExtKeyUsageClientAuth { + continue + } + + for _, emailAddress := range cert.EmailAddresses { + emailLower := strings.ToLower(emailAddress) + emailsTried[emailLower] = true + + voter, err := FindVoterByAddress(emailLower) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + if voter != nil { + requestContext := context.WithValue(r.Context(), ctxVoter, voter) + requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert) + handler(w, r.WithContext(requestContext)) + + return } } } } + needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool) if ok && needsAuth { var templateContext struct { @@ -129,14 +152,18 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(ht Flashes interface{} Emails []string } + for k := range emailsTried { templateContext.Emails = append(templateContext.Emails, k) } + sort.Strings(templateContext.Emails) w.WriteHeader(http.StatusForbidden) renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext) + return } + handler(w, r) } @@ -154,6 +181,7 @@ type motionListParameters struct { func parseMotionParameters(r *http.Request) motionParameters { var m = motionParameters{} m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes")) + return m } @@ -164,20 +192,24 @@ func parseMotionListParameters(r *http.Request) motionListParameters { } else { m.Page = page } + m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw")) m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted")) if r.Method == http.MethodPost { m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm")) } + return m } func motionListHandler(w http.ResponseWriter, r *http.Request) { params := parseMotionListParameters(r) + session, err := store.Get(r, sessionCookieName) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } @@ -189,30 +221,42 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) { PageTitle string Flashes interface{} } + if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok { templateContext.Voter = voter } + if flashes := session.Flashes(); len(flashes) > 0 { templateContext.Flashes = flashes } + err = session.Save(r, w) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + templateContext.Params = ¶ms - if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil { + if templateContext.Decisions, err = FindDecisionsForDisplayOnPage( + params.Page, params.Flags.Unvoted, templateContext.Voter, + ); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } if len(templateContext.Decisions) > 0 { - olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter) + olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists( + params.Flags.Unvoted, templateContext.Voter, + ) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + if olderExists { templateContext.NextPage = params.Page + 1 } @@ -233,6 +277,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return } @@ -244,32 +289,53 @@ func motionHandler(w http.ResponseWriter, r *http.Request) { PageTitle string Flashes interface{} } + voter, ok := getVoterFromRequest(r) if ok { templateContext.Voter = voter } + templateContext.Params = ¶ms + if params.ShowVotes { if err := decision.LoadVotes(); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } } + templateContext.Decision = decision templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title) - renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html"}, templateContext) + + renderTemplate(w, r, []string{ + "motion.html", + "motion_fragments.html", + "page_fragments.html", + "header.html", + "footer.html", + }, templateContext) } -func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) { +func singleDecisionHandler( + w http.ResponseWriter, + r *http.Request, + tag string, + handler func(http.ResponseWriter, *http.Request), +) { decision, err := FindDecisionForDisplayByTag(tag) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + if decision == nil { http.NotFound(w, r) + return } + handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision))) } @@ -286,16 +352,19 @@ func (authenticationRequiredHandler) NeedsAuth() bool { func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) { voter, ok = r.Context().Value(ctxVoter).(*Voter) + return } func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) { decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay) + return } func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) { vote, ok = r.Context().Value(ctxVote).(VoteChoice) + return } @@ -305,12 +374,16 @@ func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, me session, err := store.Get(r, sessionCookieName) if err != nil { log.Warnf("could not get session cookie: %v", err) + return } + session.AddFlash(message, tags...) + err = session.Save(r, w) if err != nil { log.Warnf("could not save flash message: %v", err) + return } } @@ -324,14 +397,25 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok || decision.Status != voteStatusPending { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return } + voter, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return } - templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"} + + templates := []string{ + "withdraw_motion_form.html", + "header.html", + "footer.html", + "motion_fragments.html", + "page_fragments.html", + } + var templateContext struct { PageTitle string Decision *DecisionForDisplay @@ -343,9 +427,11 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) { case http.MethodPost: decision.Status = voteStatusWithdrawn decision.Modified = time.Now().UTC() + if err := decision.UpdateStatus(); err != nil { log.Errorf("withdrawing motion failed: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } @@ -372,12 +458,14 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) { } templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"} + var templateContext struct { Form NewDecisionForm PageTitle string Voter *Voter Flashes interface{} } + switch r.Method { case http.MethodPost: form := NewDecisionForm{ @@ -393,10 +481,11 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) { renderTemplate(w, r, templates, templateContext) } else { data.Proposed = time.Now().UTC() - data.ProponentId = voter.Id + data.ProponentID = voter.ID if err := data.Create(); err != nil { log.Errorf("saving motion failed: %v", err) http.Error(w, "Saving motion failed", http.StatusInternalServerError) + return } @@ -426,20 +515,26 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok || decision.Status != voteStatusPending { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return } + voter, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return } + templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"} + var templateContext struct { Form EditDecisionForm PageTitle string Voter *Voter Flashes interface{} } + switch r.Method { case http.MethodPost: form := EditDecisionForm{ @@ -459,6 +554,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) { if err := data.Update(); err != nil { log.Errorf("updating motion failed: %v", err) http.Error(w, "Updating the motion failed.", http.StatusInternalServerError) + return } @@ -468,6 +564,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) } + return default: templateContext.Voter = voter @@ -494,35 +591,43 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case subURL == "": authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler) + return case subURL == "/newmotion/": handler := &newMotionHandler{} authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), handler.Handle) + return case strings.Count(subURL, "/") == 1: parts := strings.Split(subURL, "/") motionTag := parts[0] + action, ok := motionActionMap[parts[1]] if !ok { http.NotFound(w, r) + return } + authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())), func(w http.ResponseWriter, r *http.Request) { singleDecisionHandler(w, r, motionTag, action.Handle) }) + return case strings.Count(subURL, "/") == 0: authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), func(w http.ResponseWriter, r *http.Request) { singleDecisionHandler(w, r, subURL, motionHandler) }) + return default: http.NotFound(w, r) + return } } @@ -536,26 +641,33 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + voter, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + vote, ok := getVoteFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + switch r.Method { case http.MethodPost: voteResult := &Vote{ - VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(), + VoterID: voter.ID, Vote: vote, DecisionID: decision.ID, Voted: time.Now().UTC(), Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))} if err := voteResult.Save(); err != nil { log.Errorf("Problem saving vote: %v", err) http.Error(w, "Problem saving vote", http.StatusInternalServerError) + return } @@ -565,7 +677,14 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) default: - templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"} + templates := []string{ + "direct_vote_form.html", + "header.html", + "footer.html", + "motion_fragments.html", + "page_fragments.html", + } + var templateContext struct { Decision *DecisionForDisplay VoteChoice VoteChoice @@ -573,9 +692,11 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { Flashes interface{} Voter *Voter } + templateContext.Decision = decision templateContext.VoteChoice = vote templateContext.Voter = voter + renderTemplate(w, r, templates, templateContext) } } @@ -588,10 +709,12 @@ type proxyVoteHandler struct { func getPEMClientCert(r *http.Request) string { clientCertPEM := bytes.NewBufferString("") authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate) + err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw}) if err != nil { log.Errorf("error encoding client certificate: %v", err) } + return clientCertPEM.String() } @@ -599,14 +722,25 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { decision, ok := getDecisionFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + proxy, ok := getVoterFromRequest(r) if !ok { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } - templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"} + + templates := []string{ + "proxy_vote_form.html", + "header.html", + "footer.html", + "motion_fragments.html", + "page_fragments.html", + } + var templateContext struct { Form ProxyVoteForm Decision *DecisionForDisplay @@ -615,7 +749,9 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { Flashes interface{} Voter *Voter } + templateContext.Voter = proxy + switch r.Method { case http.MethodPost: form := ProxyVoteForm{ @@ -627,15 +763,19 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { if valid, voter, data, justification := form.Validate(); !valid { templateContext.Form = form templateContext.Decision = decision - if voters, err := GetVotersForProxy(proxy); err != nil { + + voters, err := GetVotersForProxy(proxy) + if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return - } else { - templateContext.Voters = voters } + + templateContext.Voters = voters + renderTemplate(w, r, templates, templateContext) } else { - data.DecisionId = decision.Id + data.DecisionID = decision.ID data.Voted = time.Now().UTC() data.Notes = fmt.Sprintf( "Proxy-Vote by %s\n\n%s\n\n%s", @@ -644,6 +784,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { if err := data.Save(); err != nil { log.Errorf("Error saving vote: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } @@ -653,16 +794,21 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) } + return default: templateContext.Form = ProxyVoteForm{} templateContext.Decision = decision - if voters, err := GetVotersForProxy(proxy); err != nil { + + voters, err := GetVotersForProxy(proxy) + if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return - } else { - templateContext.Voters = voters } + + templateContext.Voters = voters + renderTemplate(w, r, templates, templateContext) } } @@ -674,24 +820,33 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) case strings.HasPrefix(r.URL.Path, "/proxy/"): motionTag := r.URL.Path[len("/proxy/"):] handler := &proxyVoteHandler{} + authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), func(w http.ResponseWriter, r *http.Request) { singleDecisionHandler(w, r, motionTag, handler.Handle) }) case strings.HasPrefix(r.URL.Path, "/vote/"): + const expectedParts = 2 + parts := strings.Split(r.URL.Path[len("/vote/"):], "/") - if len(parts) != 2 { + if len(parts) != expectedParts { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return } + motionTag := parts[0] + voteValue, ok := VoteValues[parts[1]] if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return } + handler := &directVoteHandler{} + authenticateRequest( w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), func(w http.ResponseWriter, r *http.Request) { @@ -699,6 +854,7 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)), motionTag, handler.Handle) }) + return } } @@ -714,8 +870,8 @@ type Config struct { CookieSecret string `yaml:"cookie_secret"` CsrfKey string `yaml:"csrf_key"` BaseURL string `yaml:"base_url"` - HttpAddress string `yaml:"http_address"` - HttpsAddress string `yaml:"https_address"` + HTTPAddress string `yaml:"http_address"` + HTTPSAddress string `yaml:"https_address"` MailServer struct { Host string `yaml:"host"` Port int `yaml:"port"` @@ -727,15 +883,17 @@ func readConfig() { if err != nil { log.Panicf("Opening configuration file failed: %v", err) } + if err := yaml.Unmarshal(source, &config); err != nil { log.Panicf("Loading configuration failed: %v", err) } - if config.HttpsAddress == "" { - config.HttpsAddress = "127.0.0.1:8443" + if config.HTTPSAddress == "" { + config.HTTPSAddress = "127.0.0.1:8443" } - if config.HttpAddress == "" { - config.HttpAddress = "127.0.0.1:8080" + + if config.HTTPAddress == "" { + config.HTTPAddress = "127.0.0.1:8080" } cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret) @@ -743,17 +901,26 @@ func readConfig() { log.Panicf("Decoding cookie secret failed: %v", err) panic(err) } - if len(cookieSecret) < 32 { - log.Panic("Cookie secret is less than 32 bytes long") + + if len(cookieSecret) < cookieSecretMinLen { + log.Panicf("Cookie secret is less than %d bytes long", cookieSecretMinLen) } + csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey) if err != nil { log.Panicf("Decoding csrf key failed: %v", err) } - if len(csrfKey) != 32 { - log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey)) + + if len(csrfKey) != csrfKeyLength { + log.Panicf( + "CSRF key must be exactly %d bytes long but is %d bytes long", + csrfKeyLength, + len(csrfKey), + ) } + store = sessions.NewCookieStore(cookieSecret) + log.Info("Read configuration") } @@ -762,6 +929,7 @@ func setupDbConfig(ctx context.Context) { if err != nil { log.Panicf("Opening database failed: %v", err) } + db = NewDB(database) go func() { @@ -812,6 +980,7 @@ func setupTLSConfig() (tlsConfig *tls.Config) { if err != nil { log.Panicf("Error reading client certificate CAs %v", err) } + caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(caCert) { log.Panic("could not initialize client CA certificate pool") @@ -819,9 +988,11 @@ func setupTLSConfig() (tlsConfig *tls.Config) { // setup HTTPS server tlsConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, ClientCAs: caCertPool, ClientAuth: tls.VerifyClientCertIfGiven, } + return } @@ -835,23 +1006,26 @@ func main() { flag.Parse() var stopAll func() + executionContext, stopAll := context.WithCancel(context.Background()) + readConfig() setupDbConfig(executionContext) setupNotifications(executionContext) setupJobs(executionContext) setupHandlers() + tlsConfig := setupTLSConfig() defer stopAll() server := &http.Server{ - Addr: config.HttpsAddress, + Addr: config.HTTPSAddress, TLSConfig: tlsConfig, - IdleTimeout: time.Second * 120, - ReadHeaderTimeout: time.Second * 10, - ReadTimeout: time.Second * 20, - WriteTimeout: time.Second * 60, + IdleTimeout: time.Second * httpIdleTimeout, + ReadHeaderTimeout: time.Second * httpReadHeaderTimeout, + ReadTimeout: time.Second * httpReadTimeout, + WriteTimeout: time.Second * httpWriteTimeout, } server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux) @@ -859,24 +1033,27 @@ func main() { log.Infof("Launching application on https://%s/", server.Addr) errs := make(chan error, 1) + go func() { httpRedirector := &http.Server{ - Addr: config.HttpAddress, + Addr: config.HTTPAddress, Handler: http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently), - IdleTimeout: time.Second * 5, - ReadHeaderTimeout: time.Second * 10, - ReadTimeout: time.Second * 10, - WriteTimeout: time.Second * 60, + IdleTimeout: time.Second * httpIdleTimeout, + ReadHeaderTimeout: time.Second * httpReadHeaderTimeout, + ReadTimeout: time.Second * httpReadTimeout, + WriteTimeout: time.Second * httpWriteTimeout, } if err := httpRedirector.ListenAndServe(); err != nil { errs <- err } + close(errs) }() if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil { log.Panicf("ListenAndServerTLS failed: %v", err) } + if err := <-errs; err != nil { log.Panicf("ListenAndServe failed: %v", err) } diff --git a/boardvoting/main.go b/boardvoting/main.go index 718276e..0997faa 100644 --- a/boardvoting/main.go +++ b/boardvoting/main.go @@ -1,18 +1,20 @@ /* - Copyright 2017-2019 Jan Dittberner +Copyright 2017-2021 Jan Dittberner - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this program except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this program except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. +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. */ + +// Assets for the CAcert board voting software. package boardvoting //go:generate go-bindata -pkg $GOPACKAGE -o assets.go ./migrations/... ./static/... ./templates/... @@ -29,8 +31,11 @@ import ( "time" ) +const defaultFileMode = 0400 + var defaultFileTimestamp = time.Now() +// Simulated asset file system. type AssetFS struct { Asset func(path string) ([]byte, error) AssetDir func(path string) ([]string, error) @@ -46,16 +51,19 @@ type FakeFile struct { func (f *FakeFile) Name() string { _, name := filepath.Split(f.Path) + return name } func (f *FakeFile) Size() int64 { return f.Len } func (f *FakeFile) Mode() os.FileMode { - mode := os.FileMode(0644) + mode := os.FileMode(defaultFileMode) + if f.Dir { return mode | os.ModeDir } + return mode } @@ -73,6 +81,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile { if timestamp.IsZero() { timestamp = defaultFileTimestamp } + return &AssetFile{ bytes.NewReader(content), ioutil.NopCloser(nil), @@ -80,7 +89,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile { } } -func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) { +func (f *AssetFile) Readdir(int) ([]os.FileInfo, error) { return nil, errors.New("not a directory") } @@ -102,24 +111,31 @@ func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) { if count <= 0 { return f.Children, nil } + if f.ChildrenRead+count > len(f.Children) { count = len(f.Children) - f.ChildrenRead } + rv := f.Children[f.ChildrenRead : f.ChildrenRead+count] f.ChildrenRead += count + return rv, nil } +// Simulate os.Stat for a simulated asset directory. func (f *AssetDirectory) Stat() (os.FileInfo, error) { return f, nil } +// Return a new asset directory. func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory { fileInfos := make([]os.FileInfo, 0, len(children)) + for _, child := range children { _, err := fs.AssetDir(filepath.Join(name, child)) fileInfos = append(fileInfos, &FakeFile{child, err == nil, 0, time.Time{}}) } + return &AssetDirectory{ AssetFile{ bytes.NewReader(nil), @@ -130,29 +146,37 @@ func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirect fileInfos} } +// Open named simulated file. func (f *AssetFS) Open(name string) (http.File, error) { if len(name) > 0 && name[0] == '/' { name = name[1:] } + if b, err := f.Asset(name); err == nil { timestamp := defaultFileTimestamp + if f.AssetInfo != nil { if info, err := f.AssetInfo(name); err == nil { timestamp = info.ModTime() } } + return NewAssetFile(name, b, timestamp), nil } - if children, err := f.AssetDir(name); err == nil { + + children, err := f.AssetDir(name) + if err == nil { return NewAssetDirectory(name, children, f), nil - } else { - if strings.Contains(err.Error(), "not found") { - return nil, os.ErrNotExist - } - return nil, err } + + if strings.Contains(err.Error(), "not found") { + return nil, os.ErrNotExist + } + + return nil, err } +// Get the simulated asset filesystem initialized with generated values. func GetAssetFS() *AssetFS { return &AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo} } diff --git a/db/migrations_embedded.go b/db/migrations_embedded.go index 8aa355a..d95b6d9 100644 --- a/db/migrations_embedded.go +++ b/db/migrations_embedded.go @@ -1,23 +1,24 @@ /* - Copyright 2017-2019 Jan Dittberner +Copyright 2017-2020 Jan Dittberner - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this program except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this program except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. +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 db import ( + migrate "github.com/rubenv/sql-migrate" + "git.cacert.org/cacert-boardvoting/boardvoting" - "github.com/rubenv/sql-migrate" ) func Migrations() migrate.MigrationSource { diff --git a/forms.go b/forms.go index 9af6cf1..8d573bc 100644 --- a/forms.go +++ b/forms.go @@ -1,18 +1,19 @@ /* - Copyright 2017-2019 Jan Dittberner +Copyright 2017-2021 Jan Dittberner - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this program except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this program except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. +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 ( @@ -22,11 +23,24 @@ import ( "time" ) +const ( + minimumContentLen = 3 + minimumTitleLen = 3 +) + +const ( + hoursInADay = 24 + dueThreeDays = 3 + dueOneWeek = 7 + dueTwoWeeks = 14 + dueFourWeeks = 28 +) + var validDueDurations = map[string]time.Duration{ - "+3 days": time.Hour * 24 * 3, - "+7 days": time.Hour * 24 * 7, - "+14 days": time.Hour * 24 * 14, - "+28 days": time.Hour * 24 * 28, + "+3 days": time.Hour * hoursInADay * dueThreeDays, + "+7 days": time.Hour * hoursInADay * dueOneWeek, + "+14 days": time.Hour * hoursInADay * dueTwoWeeks, + "+28 days": time.Hour * hoursInADay * dueFourWeeks, } type NewDecisionForm struct { @@ -43,13 +57,13 @@ func (f *NewDecisionForm) Validate() (bool, *Decision) { data := &Decision{} data.Title = strings.TrimSpace(f.Title) - if len(data.Title) < 3 { - f.Errors["Title"] = "Please enter at least 3 characters for Title." + if len(data.Title) < minimumTitleLen { + f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen) } data.Content = strings.TrimSpace(f.Content) - if len(strings.Fields(data.Content)) < 3 { - f.Errors["Content"] = "Please enter at least 3 words as Text." + if len(strings.Fields(data.Content)) < minimumContentLen { + f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen) } if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) { @@ -83,13 +97,13 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) { data := f.Decision data.Title = strings.TrimSpace(f.Title) - if len(data.Title) < 3 { - f.Errors["Title"] = "Please enter at least 3 characters for Title." + if len(data.Title) < minimumTitleLen { + f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen) } data.Content = strings.TrimSpace(f.Content) - if len(strings.Fields(data.Content)) < 3 { - f.Errors["Content"] = "Please enter at least 3 words as Text." + if len(strings.Fields(data.Content)) < minimumContentLen { + f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen) } if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) { @@ -118,27 +132,34 @@ type ProxyVoteForm struct { func (f *ProxyVoteForm) Validate() (bool, *Voter, *Vote, string) { f.Errors = make(map[string]string) + const minimumJustificationLen = 3 + + var ( + voter *Voter + err error + voterID, vote int64 + ) + data := &Vote{} - var voter *Voter - if voterId, err := strconv.ParseInt(f.Voter, 10, 64); err != nil { - f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err) - } else if voter, err = GetVoterById(voterId); err != nil { - f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err) + if voterID, err = strconv.ParseInt(f.Voter, 10, 64); err != nil { + f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err) + } else if voter, err = GetVoterByID(voterID); err != nil { + f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err) } else { - data.VoterId = voter.Id + data.VoterID = voter.ID } - if vote, err := strconv.ParseInt(f.Vote, 10, 8); err != nil { - f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.", err) + if vote, err = strconv.ParseInt(f.Vote, 10, 8); err != nil { + f.Errors["Vote"] = fmt.Sprintf("Please choose a valid vote: %v.", err) } else if voteChoice, ok := VoteChoices[vote]; !ok { - f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.") + f.Errors["Vote"] = "Please choose a valid vote." } else { data.Vote = voteChoice } justification := strings.TrimSpace(f.Justification) - if len(justification) < 3 { + if len(justification) < minimumJustificationLen { f.Errors["Justification"] = "Please enter at least 3 characters for justification." } diff --git a/go.mod b/go.mod index 9aa52f8..02572a1 100644 --- a/go.mod +++ b/go.mod @@ -4,23 +4,26 @@ go 1.14 require ( github.com/Masterminds/goutils v1.1.0 // indirect - github.com/Masterminds/semver v1.4.2 // indirect - github.com/Masterminds/sprig v2.20.0+incompatible + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible github.com/gobuffalo/packr v1.30.1 // indirect - github.com/google/uuid v1.1.1 // indirect - github.com/gorilla/csrf v1.6.0 - github.com/gorilla/sessions v1.2.0 - github.com/huandu/xstrings v1.2.0 // indirect - github.com/imdario/mergo v0.3.7 // indirect + github.com/google/uuid v1.1.4 // indirect + github.com/gorilla/csrf v1.7.0 + github.com/gorilla/sessions v1.2.1 + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.11 // indirect github.com/jmoiron/sqlx v1.2.0 - github.com/mattn/go-sqlite3 v1.11.0 - github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079 - github.com/sirupsen/logrus v1.4.2 + github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect + github.com/mattn/go-sqlite3 v1.14.6 + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.1 // indirect + github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 + github.com/sirupsen/logrus v1.7.0 github.com/ziutek/mymysql v1.5.4 // indirect - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect - google.golang.org/appengine v1.6.1 // indirect + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect + golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gorp.v1 v1.7.2 // indirect - gopkg.in/yaml.v2 v2.2.2 + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index caf79c2..e900429 100644 --- a/go.sum +++ b/go.sum @@ -1,52 +1,191 @@ +cloud.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/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.20.0+incompatible h1:dJTKKuUkYW3RMFdQFXPU/s6hg10RgctmTjRcbZ98Ap8= github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +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/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/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/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +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/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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 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 v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +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/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +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/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/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-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-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= +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/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +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/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/snappy v0.0.0-20180518054509-2e65f85255db/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/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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0= +github.com/google/uuid v1.1.4/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/csrf v1.6.0 h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0= github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y= +github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= +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/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +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-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/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 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/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 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/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/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/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 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= @@ -54,75 +193,305 @@ 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/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +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-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/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/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= +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.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/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_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/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/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/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.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079 h1:xPeaaIHjF9j8jbYQ5xdvLnFp+lpmGYFG1uBPtXNBHno= github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +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/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +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/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 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/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +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/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +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.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-20181203042331-505ab145d0a9/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-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-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-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/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-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/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-20181205085412-a5c9d58dba9a/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-20190412213103-97732733099d/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-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s= +golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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/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-20190114222345-bf090417da8b/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/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-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +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/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/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +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= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/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= diff --git a/jobs.go b/jobs.go index 096afed..9f43902 100644 --- a/jobs.go +++ b/jobs.go @@ -1,23 +1,25 @@ /* - Copyright 2017-2019 Jan Dittberner +Copyright 2017-2021 Jan Dittberner - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this program except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this program except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. +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 ( - log "github.com/sirupsen/logrus" "time" + + log "github.com/sirupsen/logrus" ) type Job interface { @@ -29,30 +31,34 @@ type Job interface { type jobIdentifier int const ( - JobIdCloseDecisions jobIdentifier = iota - JobIdRemindVotersJob + JobIDCloseDecisions jobIdentifier = iota + JobIDRemindVotersJob ) var rescheduleChannel = make(chan jobIdentifier, 1) func JobScheduler(quitChannel chan int) { var jobs = map[jobIdentifier]Job{ - JobIdCloseDecisions: NewCloseDecisionsJob(), - JobIdRemindVotersJob: NewRemindVotersJob(), + JobIDCloseDecisions: NewCloseDecisionsJob(), + JobIDRemindVotersJob: NewRemindVotersJob(), } + log.Info("started job scheduler") for { select { - case jobId := <-rescheduleChannel: - job := jobs[jobId] + case jobID := <-rescheduleChannel: + job := jobs[jobID] + log.Infof("reschedule job %s", job) job.Schedule() case <-quitChannel: for _, job := range jobs { job.Stop() } + log.Info("stop job scheduler") + return } } @@ -65,27 +71,35 @@ type CloseDecisionsJob struct { func NewCloseDecisionsJob() *CloseDecisionsJob { job := &CloseDecisionsJob{} job.Schedule() + return job } func (j *CloseDecisionsJob) Schedule() { - var nextDue *time.Time - nextDue, err := GetNextPendingDecisionDue() + var ( + nextDue *time.Time + err error + ) + + nextDue, err = GetNextPendingDecisionDue() if err != nil { log.Error("Could not get next pending due date") + if j.timer != nil { j.timer.Stop() j.timer = nil } + return } + if nextDue == nil { log.Info("no next planned execution of CloseDecisionsJob") j.Stop() } else { nextDue := nextDue.Add(time.Second) log.Infof("scheduling CloseDecisionsJob for %s", nextDue) - when := nextDue.Sub(time.Now()) + when := time.Until(nextDue) if j.timer != nil { j.timer.Reset(when) } else { @@ -103,11 +117,13 @@ func (j *CloseDecisionsJob) Stop() { func (j *CloseDecisionsJob) Run() { log.Debug("running CloseDecisionsJob") + err := CloseDecisions() if err != nil { log.Errorf("closing decisions %v", err) } - rescheduleChannel <- JobIdCloseDecisions + + rescheduleChannel <- JobIDCloseDecisions } func (j *CloseDecisionsJob) String() string { @@ -121,14 +137,18 @@ type RemindVotersJob struct { func NewRemindVotersJob() *RemindVotersJob { job := &RemindVotersJob{} job.Schedule() + return job } func (j *RemindVotersJob) Schedule() { year, month, day := time.Now().UTC().Date() nextExecution := time.Date(year, month, day, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 3) + log.Infof("scheduling RemindVotersJob for %s", nextExecution) - when := nextExecution.Sub(time.Now()) + + when := time.Until(nextExecution) + if j.timer != nil { j.timer.Reset(when) } else { @@ -145,22 +165,33 @@ func (j *RemindVotersJob) Stop() { func (j *RemindVotersJob) Run() { log.Info("running RemindVotersJob") - defer func() { rescheduleChannel <- JobIdRemindVotersJob }() - voters, err := GetReminderVoters() + defer func() { rescheduleChannel <- JobIDRemindVotersJob }() + + var ( + voters []Voter + err error + ) + + voters, err = GetReminderVoters() if err != nil { log.Errorf("problem getting voters %v", err) + return } - for _, voter := range *voters { - decisions, err := FindUnvotedDecisionsForVoter(&voter) + var decisions []Decision + + for i := range voters { + decisions, err = FindUnVotedDecisionsForVoter(&voters[i]) if err != nil { log.Errorf("problem getting unvoted decisions: %v", err) + return } - if len(*decisions) > 0 { - NotifyMailChannel <- &RemindVoterNotification{voter: voter, decisions: *decisions} + + if len(decisions) > 0 { + NotifyMailChannel <- &RemindVoterNotification{voter: voters[i], decisions: decisions} } } } diff --git a/models.go b/models.go index 2ce7db4..3647097 100644 --- a/models.go +++ b/models.go @@ -1,51 +1,54 @@ /* - Copyright 2017-2020 Jan Dittberner +Copyright 2017-2021 Jan Dittberner - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this program except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this program except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. +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 ( "database/sql" + "errors" "fmt" - "github.com/jmoiron/sqlx" - "github.com/rubenv/sql-migrate" "time" - migrations "git.cacert.org/cacert-boardvoting/db" + "github.com/jmoiron/sqlx" + migrate "github.com/rubenv/sql-migrate" log "github.com/sirupsen/logrus" + + migrations "git.cacert.org/cacert-boardvoting/db" ) type sqlKey int const ( sqlLoadDecisions sqlKey = iota - sqlLoadUnvotedDecisions + sqlLoadUnVotedDecisions sqlLoadDecisionByTag - sqlLoadDecisionById + sqlLoadDecisionByID sqlLoadVoteCountsForDecision sqlLoadVotesForDecision sqlLoadEnabledVoterByEmail sqlCountOlderThanDecision - sqlCountOlderThanUnvotedDecision + sqlCountOlderThanUnVotedDecision sqlCreateDecision sqlUpdateDecision sqlUpdateDecisionStatus sqlSelectClosableDecisions sqlGetNextPendingDecisionDue sqlGetReminderVoters - sqlFindUnvotedDecisionsForVoter - sqlGetEnabledVoterById + sqlFindUnVotedDecisionsForVoter + sqlGetEnabledVoterByID sqlCreateVote sqlLoadVote sqlGetVotersForProxy @@ -59,7 +62,7 @@ FROM decisions JOIN voters ON decisions.proponent=voters.id ORDER BY proposed DESC LIMIT 10 OFFSET 10 * $1`, - sqlLoadUnvotedDecisions: ` + sqlLoadUnVotedDecisions: ` SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title, decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified FROM decisions @@ -73,7 +76,7 @@ SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer FROM decisions JOIN voters ON decisions.proponent=voters.id WHERE decisions.tag=$1;`, - sqlLoadDecisionById: ` + sqlLoadDecisionByID: ` SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified FROM decisions @@ -91,14 +94,14 @@ FROM voters JOIN emails ON voters.id=emails.voter JOIN user_roles ON user_roles.voter_id=voters.id WHERE emails.address=$1 AND user_roles.role='VOTER'`, - sqlGetEnabledVoterById: ` + sqlGetEnabledVoterByID: ` SELECT voters.id, voters.name, voters.reminder FROM voters JOIN user_roles ON user_roles.voter_id=voters.id WHERE user_roles.role='VOTER' AND voters.id=$1`, sqlCountOlderThanDecision: ` SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`, - sqlCountOlderThanUnvotedDecision: ` + sqlCountOlderThanUnVotedDecision: ` SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`, sqlCreateDecision: ` @@ -134,7 +137,7 @@ SELECT voters.id, voters.name, voters.reminder FROM voters JOIN user_roles ON user_roles.voter_id=voters.id WHERE user_roles.role='VOTER' AND reminder!='' AND reminder IS NOT NULL`, - sqlFindUnvotedDecisionsForVoter: ` + sqlFindUnVotedDecisionsForVoter: ` SELECT tag, title, votetype, due FROM decisions WHERE status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1) @@ -152,9 +155,9 @@ type VoteType uint8 type VoteStatus int8 type Decision struct { - Id int64 + ID int64 `db:"id"` Proposed time.Time - ProponentId int64 `db:"proponent"` + ProponentID int64 `db:"proponent"` Title string Content string Quorum int @@ -167,7 +170,7 @@ type Decision struct { } type Voter struct { - Id int64 + ID int64 `db:"id"` Name string Reminder string // reminder email address } @@ -185,23 +188,36 @@ const ( voteTypeVeto = 1 ) +const ( + voteTypeLabelMotion = "motion" + voteTypeLabelUnknown = "unknown" + voteTypeLabelVeto = "veto" +) + func (v VoteType) String() string { switch v { case voteTypeMotion: - return "motion" + return voteTypeLabelMotion case voteTypeVeto: - return "veto" + return voteTypeLabelVeto default: - return "unknown" + return voteTypeLabelUnknown } } -func (v VoteType) QuorumAndMajority() (int, int) { +func (v VoteType) QuorumAndMajority() (int, float32) { + const ( + majorityDefault = 0.99 + majorityMotion = 0.50 + quorumDefault = 1 + quorumMotion = 3 + ) + switch v { case voteTypeMotion: - return 3, 50 + return quorumMotion, majorityMotion default: - return 1, 99 + return quorumDefault, majorityDefault } } @@ -253,36 +269,42 @@ func (v VoteStatus) String() string { } type Vote struct { - DecisionId int64 `db:"decision"` - VoterId int64 `db:"voter"` + DecisionID int64 `db:"decision"` + VoterID int64 `db:"voter"` Vote VoteChoice Voted time.Time Notes string } -type dbHandler struct { +type DbHandler struct { db *sqlx.DB } -var db *dbHandler +var db *DbHandler + +func NewDB(database *sql.DB) *DbHandler { + handler := &DbHandler{db: sqlx.NewDb(database, "sqlite3")} -func NewDB(database *sql.DB) *dbHandler { - handler := &dbHandler{db: sqlx.NewDb(database, "sqlite3")} _, err := migrate.Exec(database, "sqlite3", migrations.Migrations(), migrate.Up) if err != nil { log.Panicf("running database migration failed: %v", err) } failedStatements := make([]string, 0) + for _, sqlStatement := range sqlStatements { var stmt *sqlx.Stmt + stmt, err := handler.db.Preparex(sqlStatement) if err != nil { log.Errorf("error parsing statement %s: %s", sqlStatement, err) failedStatements = append(failedStatements, sqlStatement) } + + // nolint:sqlclosecheck _ = stmt.Close() } + if len(failedStatements) > 0 { log.Panicf("%d statements failed to prepare", len(failedStatements)) } @@ -290,44 +312,48 @@ func NewDB(database *sql.DB) *dbHandler { return handler } -func (d *dbHandler) Close() error { +func (d *DbHandler) Close() error { return d.db.Close() } -func (d *dbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt { +func (d *DbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt { statement, err := d.db.PrepareNamed(sqlStatements[statementKey]) if err != nil { log.Panicf("Preparing statement failed: %v", err) } + return statement } -func (d *dbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt { +func (d *DbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt { statement, err := d.db.Preparex(sqlStatements[statementKey]) if err != nil { log.Panicf("Preparing statement failed: %v", err) } + return statement } -func (v *Vote) Save() (err error) { +func (v *Vote) Save() error { insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote) + defer func() { _ = insertVoteStmt.Close() }() + var err error + if _, err = insertVoteStmt.Exec(v); err != nil { - log.Errorf("saving vote failed: %v", err) - return + return fmt.Errorf("saving vote failed: %w", err) } getVoteStmt := db.getPreparedStatement(sqlLoadVote) + defer func() { _ = getVoteStmt.Close() }() - if err = getVoteStmt.Get(v, v.DecisionId, v.VoterId); err != nil { - log.Errorf("getting inserted vote failed: %v", err) - return + if err = getVoteStmt.Get(v, v.DecisionID, v.VoterID); err != nil { + return fmt.Errorf("getting inserted vote failed: %w", err) } - return + return nil } type VoteSums struct { @@ -349,18 +375,20 @@ func (v *VoteSums) Percent() int { if totalVotes == 0 { return 0 } + return v.Ayes * 100 / totalVotes } -func (v *VoteSums) CalculateResult(quorum int, majority int) (status VoteStatus, reasoning string) { +func (v *VoteSums) CalculateResult(quorum int, majority float32) (VoteStatus, string) { if v.VoteCount() < quorum { - status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum) - } else if (v.Ayes / v.TotalVotes()) < (majority / 100) { - status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed majority of %d%% has not been reached.", majority) - } else { - status, reasoning = voteStatusApproved, "Quorum and majority have been reached" + return voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum) } - return + + if (float32(v.Ayes) / float32(v.TotalVotes())) < majority { + return voteStatusDeclined, fmt.Sprintf("Needed majority of %0.2f%% has not been reached.", majority) + } + + return voteStatusApproved, "Quorum and majority have been reached" } type VoteForDisplay struct { @@ -375,23 +403,26 @@ type DecisionForDisplay struct { Votes []VoteForDisplay } -func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) { +func FindDecisionForDisplayByTag(tag string) (*DecisionForDisplay, error) { decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag) + defer func() { _ = decisionStmt.Close() }() - decision = &DecisionForDisplay{} + decision := &DecisionForDisplay{} + + var err error + if err = decisionStmt.Get(decision, tag); err != nil { - if err == sql.ErrNoRows { - decision = nil - err = nil - return - } else { - log.Errorf("getting motion %s failed: %v", tag, err) - return + if errors.Is(err, sql.ErrNoRows) { + return nil, nil } + + return nil, fmt.Errorf("getting motion %s failed: %w", tag, err) } + decision.VoteSums, err = decision.Decision.VoteSums() - return + + return decision, err } // FindDecisionsForDisplayOnPage loads a set of decisions from the database. @@ -399,61 +430,78 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err // This function uses OFFSET for pagination which is not a good idea for larger data sets. // // TODO: migrate to timestamp base pagination -func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) { +func FindDecisionsForDisplayOnPage(page int64, unVoted bool, voter *Voter) ([]*DecisionForDisplay, error) { var decisionsStmt *sqlx.Stmt - if unvoted && voter != nil { - decisionsStmt = db.getPreparedStatement(sqlLoadUnvotedDecisions) + + if unVoted && voter != nil { + decisionsStmt = db.getPreparedStatement(sqlLoadUnVotedDecisions) } else { decisionsStmt = db.getPreparedStatement(sqlLoadDecisions) } + defer func() { _ = decisionsStmt.Close() }() - var rows *sqlx.Rows - if unvoted && voter != nil { - rows, err = decisionsStmt.Queryx(voter.Id, page-1) + var ( + rows *sqlx.Rows + err error + decisions []*DecisionForDisplay + ) + + if unVoted && voter != nil { + rows, err = decisionsStmt.Queryx(voter.ID, page-1) } else { rows, err = decisionsStmt.Queryx(page - 1) } + if err != nil { - log.Errorf("loading motions for page %d failed: %v", page, err) - return + return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err) } + defer func() { _ = rows.Close() }() for rows.Next() { var d DecisionForDisplay + if err = rows.StructScan(&d); err != nil { - log.Errorf("loading motions for page %d failed: %v", page, err) - return + return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err) } + d.VoteSums, err = d.Decision.VoteSums() + if err != nil { - return + return nil, err } + decisions = append(decisions, &d) } - return + + return decisions, nil } -func (d *Decision) VoteSums() (sums *VoteSums, err error) { +func (d *Decision) VoteSums() (*VoteSums, error) { votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision) + defer func() { _ = votesStmt.Close() }() - voteRows, err := votesStmt.Queryx(d.Id) + voteRows, err := votesStmt.Queryx(d.ID) if err != nil { - log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) - return + return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err) } + defer func() { _ = voteRows.Close() }() - sums = &VoteSums{} + sums := &VoteSums{} + for voteRows.Next() { - var vote VoteChoice - var count int + var ( + vote VoteChoice + count int + ) + if err = voteRows.Scan(&vote, &count); err != nil { - log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) - return + return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err) } + switch vote { case voteAye: sums.Ayes = count @@ -463,79 +511,86 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) { sums.Abstains = count } } - return + + return sums, nil } func (d *DecisionForDisplay) LoadVotes() (err error) { votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision) + defer func() { _ = votesStmt.Close() }() - err = votesStmt.Select(&d.Votes, d.Id) + err = votesStmt.Select(&d.Votes, d.ID) if err != nil { log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err) + return } + return } -func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) { +func (d *Decision) OlderExists(unvoted bool, voter *Voter) (bool, error) { + var result bool + if unvoted && voter != nil { - olderStmt := db.getPreparedStatement(sqlCountOlderThanUnvotedDecision) + olderStmt := db.getPreparedStatement(sqlCountOlderThanUnVotedDecision) + defer func() { _ = olderStmt.Close() }() - if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil { - log.Errorf("finding older motions than %s failed: %v", d.Tag, err) - return + if err := olderStmt.Get(&result, d.Proposed, voter.ID); err != nil { + return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err) } } else { olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision) + defer func() { _ = olderStmt.Close() }() - if err = olderStmt.Get(&result, d.Proposed); err != nil { - log.Errorf("finding older motions than %s failed: %v", d.Tag, err) - return + if err := olderStmt.Get(&result, d.Proposed); err != nil { + return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err) } } - return + return result, nil } -func (d *Decision) Create() (err error) { +func (d *Decision) Create() error { insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision) + defer func() { _ = insertDecisionStmt.Close() }() result, err := insertDecisionStmt.Exec(d) if err != nil { - log.Errorf("creating motion failed: %v", err) - return + return fmt.Errorf("creating motion failed: %w", err) } - lastInsertId, err := result.LastInsertId() + decisionID, err := result.LastInsertId() if err != nil { - log.Errorf("getting id of inserted motion failed: %v", err) - return + return fmt.Errorf("getting id of inserted motion failed: %w", err) } - rescheduleChannel <- JobIdCloseDecisions + rescheduleChannel <- JobIDCloseDecisions + + getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID) - getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById) defer func() { _ = getDecisionStmt.Close() }() - err = getDecisionStmt.Get(d, lastInsertId) + err = getDecisionStmt.Get(d, decisionID) if err != nil { - log.Errorf("getting inserted motion failed: %v", err) - return + return fmt.Errorf("getting inserted motion failed: %w", err) } - return + return nil } -func (d *Decision) LoadWithId() (err error) { - getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById) +func (d *Decision) LoadWithID() (err error) { + getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID) + defer func() { _ = getDecisionStmt.Close() }() - err = getDecisionStmt.Get(d, d.Id) + err = getDecisionStmt.Get(d, d.ID) if err != nil { log.Errorf("loading updated motion failed: %v", err) + return } @@ -544,93 +599,110 @@ func (d *Decision) LoadWithId() (err error) { func (d *Decision) Update() (err error) { updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision) + defer func() { _ = updateDecisionStmt.Close() }() result, err := updateDecisionStmt.Exec(d) if err != nil { log.Errorf("updating motion failed: %v", err) + return } + affectedRows, err := result.RowsAffected() if err != nil { log.Error("Problem determining the affected rows") + return } else if affectedRows != 1 { log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) } - rescheduleChannel <- JobIdCloseDecisions - err = d.LoadWithId() + rescheduleChannel <- JobIDCloseDecisions + + err = d.LoadWithID() + return } -func (d *Decision) UpdateStatus() (err error) { +func (d *Decision) UpdateStatus() error { updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus) + defer func() { _ = updateStatusStmt.Close() }() result, err := updateStatusStmt.Exec(d) if err != nil { - log.Errorf("setting motion status failed: %v", err) - return + return fmt.Errorf("setting motion status failed: %w", err) } + affectedRows, err := result.RowsAffected() if err != nil { - log.Errorf("determining the affected rows failed: %v", err) - return + return fmt.Errorf("determining the affected rows failed: %w", err) } else if affectedRows != 1 { log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) } - rescheduleChannel <- JobIdCloseDecisions - err = d.LoadWithId() - return + rescheduleChannel <- JobIDCloseDecisions + + err = d.LoadWithID() + + return err } func (d *Decision) String() string { - return fmt.Sprintf("%s %s (Id %d)", d.Tag, d.Title, d.Id) + return fmt.Sprintf("%s %s (ID %d)", d.Tag, d.Title, d.ID) } -func FindVoterByAddress(emailAddress string) (voter *Voter, err error) { +func FindVoterByAddress(emailAddress string) (*Voter, error) { findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail) + defer func() { _ = findVoterStmt.Close() }() - voter = &Voter{} - if err = findVoterStmt.Get(voter, emailAddress); err != nil { - if err != sql.ErrNoRows { - log.Errorf("getting voter for address %s failed: %v", emailAddress, err) - } else { - err = nil - voter = nil + voter := &Voter{} + if err := findVoterStmt.Get(voter, emailAddress); err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("getting voter for address %s failed: %w", emailAddress, err) } + + voter = nil } - return + + return voter, nil } func (d *Decision) Close() error { quorum, majority := d.VoteType.QuorumAndMajority() - var voteSums *VoteSums - var err error + var ( + voteSums *VoteSums + err error + ) if voteSums, err = d.VoteSums(); err != nil { log.Errorf("getting vote sums failed: %v", err) + return err } + var reasoning string + d.Status, reasoning = voteSums.CalculateResult(quorum, majority) closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus) + defer func() { _ = closeDecisionStmt.Close() }() result, err := closeDecisionStmt.Exec(d) if err != nil { - log.Errorf("closing vote failed: %v", err) - return err + return fmt.Errorf("closing vote failed: %w", err) } - if affectedRows, err := result.RowsAffected(); err != nil { - log.Errorf("getting affected rows failed: %v", err) - return err - } else if affectedRows != 1 { + + affectedRows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("getting affected rows failed: %w", err) + } + + if affectedRows != 1 { log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) } @@ -641,110 +713,117 @@ func (d *Decision) Close() error { return nil } -func CloseDecisions() (err error) { +func CloseDecisions() error { getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions) + defer func() { _ = getClosableDecisionsStmt.Close() }() decisions := make([]*Decision, 0) + rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()}) if err != nil { - log.Errorf("fetching closable decisions failed: %v", err) - return + return fmt.Errorf("fetching closable decisions failed: %w", err) } + defer func() { _ = rows.Close() }() + for rows.Next() { decision := &Decision{} if err = rows.StructScan(decision); err != nil { - log.Errorf("scanning row failed: %v", err) - return + return fmt.Errorf("scanning row failed: %w", err) } + decisions = append(decisions, decision) } + defer func() { _ = rows.Close() }() for _, decision := range decisions { log.Infof("found closable decision %s", decision.Tag) + if err = decision.Close(); err != nil { - log.Errorf("closing decision %s failed: %s", decision.Tag, err) - return + return fmt.Errorf("closing decision %s failed: %w", decision.Tag, err) } } - return + return nil } -func GetNextPendingDecisionDue() (due *time.Time, err error) { +func GetNextPendingDecisionDue() (*time.Time, error) { getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue) + defer func() { _ = getNextPendingDecisionDueStmt.Close() }() row := getNextPendingDecisionDueStmt.QueryRow() - due = &time.Time{} - if err = row.Scan(due); err != nil { - if err == sql.ErrNoRows { + due := &time.Time{} + if err := row.Scan(due); err != nil { + if errors.Is(err, sql.ErrNoRows) { log.Debug("No pending decisions") + return nil, nil } - log.Errorf("parsing result failed: %v", err) - return nil, err + + return nil, fmt.Errorf("parsing result failed: %w", err) } - return + return due, nil } -func GetReminderVoters() (voters *[]Voter, err error) { +func GetReminderVoters() ([]Voter, error) { getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters) + defer func() { _ = getReminderVotersStmt.Close() }() - voterSlice := make([]Voter, 0) + var voters []Voter - if err = getReminderVotersStmt.Select(&voterSlice); err != nil { - log.Errorf("getting voters failed: %v", err) - return + if err := getReminderVotersStmt.Select(&voters); err != nil { + return nil, fmt.Errorf("getting voters failed: %w", err) } - voters = &voterSlice - return + return voters, nil } -func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) { - findUnvotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnvotedDecisionsForVoter) - defer func() { _ = findUnvotedDecisionsForVoterStmt.Close() }() +func FindUnVotedDecisionsForVoter(voter *Voter) ([]Decision, error) { + findUnVotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnVotedDecisionsForVoter) - decisionsSlice := make([]Decision, 0) + defer func() { _ = findUnVotedDecisionsForVoterStmt.Close() }() - if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil { - log.Errorf("getting unvoted decisions failed: %v", err) - return + var decisions []Decision + + if err := findUnVotedDecisionsForVoterStmt.Select(&decisions, voter.ID); err != nil { + return nil, fmt.Errorf("getting unvoted decisions failed: %w", err) } - decisions = &decisionsSlice - return + return decisions, nil } -func GetVoterById(id int64) (voter *Voter, err error) { - getVoterByIdStmt := db.getPreparedStatement(sqlGetEnabledVoterById) - defer func() { _ = getVoterByIdStmt.Close() }() +func GetVoterByID(id int64) (*Voter, error) { + getVoterByIDStmt := db.getPreparedStatement(sqlGetEnabledVoterByID) - voter = &Voter{} - if err = getVoterByIdStmt.Get(voter, id); err != nil { - log.Errorf("getting voter failed: %v", err) - return + defer func() { _ = getVoterByIDStmt.Close() }() + + voter := &Voter{} + if err := getVoterByIDStmt.Get(voter, id); err != nil { + return nil, fmt.Errorf("getting voter failed: %w", err) } - return + return voter, nil } func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) { getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy) + defer func() { _ = getVotersForProxyStmt.Close() }() votersSlice := make([]Voter, 0) - if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id); err != nil { + if err = getVotersForProxyStmt.Select(&votersSlice, proxy.ID); err != nil { log.Errorf("Error getting voters for proxy failed: %v", err) + return } + voters = &votersSlice return diff --git a/notifications.go b/notifications.go index ed77573..49fc543 100644 --- a/notifications.go +++ b/notifications.go @@ -1,27 +1,30 @@ /* - Copyright 2017-2019 Jan Dittberner +Copyright 2017-2021 Jan Dittberner - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this program except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this program except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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. +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" "fmt" - "git.cacert.org/cacert-boardvoting/boardvoting" + "text/template" + "github.com/Masterminds/sprig" "gopkg.in/gomail.v2" - "text/template" + + "git.cacert.org/cacert-boardvoting/boardvoting" log "github.com/sirupsen/logrus" ) @@ -37,7 +40,7 @@ type recipientData struct { field, address, name string } -type notificationContent struct { +type NotificationContent struct { template string data interface{} subject string @@ -46,32 +49,39 @@ type notificationContent struct { } type NotificationMail interface { - GetNotificationContent() *notificationContent + GetNotificationContent() *NotificationContent } var NotifyMailChannel = make(chan NotificationMail, 1) func MailNotifier(quitMailNotifier chan int) { log.Info("Launched mail notifier") + for { select { case notification := <-NotifyMailChannel: content := notification.GetNotificationContent() + mailText, err := buildMail(content.template, content.data) if err != nil { log.Errorf("building mail failed: %v", err) + continue } m := gomail.NewMessage() m.SetAddressHeader("From", config.NotificationSenderAddress, "CAcert board voting system") + for _, recipient := range content.recipients { m.SetAddressHeader(recipient.field, recipient.address, recipient.name) } + m.SetHeader("Subject", content.subject) + for _, header := range content.headers { m.SetHeader(header.name, header.value...) } + m.SetBody("text/plain", mailText.String()) d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "") @@ -80,6 +90,7 @@ func MailNotifier(quitMailNotifier chan int) { } case <-quitMailNotifier: log.Info("Ending mail notifier") + return } } @@ -90,6 +101,7 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer if err != nil { return } + t, err := template.New(templateName).Funcs(sprig.GenericFuncMap()).Parse(string(b)) if err != nil { return @@ -97,8 +109,10 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer mailText = bytes.NewBufferString("") if err := t.Execute(mailText, context); err != nil { - log.Errorf("Failed to execute template %s with context %+v: %v", templateName, context, err) - return nil, err + return nil, fmt.Errorf( + "failed to execute template %s with context %+v: %w", + templateName, context, err, + ) } return @@ -122,6 +136,7 @@ func (n *decisionReplyBase) getHeaders() headerList { headers = append(headers, headerData{ name: "In-Reply-To", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)}, }) + return headers } @@ -139,11 +154,12 @@ type notificationClosedDecision struct { func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums, reasoning string) NotificationMail { notification := ¬ificationClosedDecision{voteSums: *voteSums, reasoning: reasoning} notification.decision = *decision + return notification } -func (n *notificationClosedDecision) GetNotificationContent() *notificationContent { - return ¬ificationContent{ +func (n *notificationClosedDecision) GetNotificationContent() *NotificationContent { + return &NotificationContent{ template: "closed_motion_mail.txt", data: struct { *Decision @@ -162,10 +178,11 @@ type NotificationCreateMotion struct { voter Voter } -func (n *NotificationCreateMotion) GetNotificationContent() *notificationContent { +func (n *NotificationCreateMotion) GetNotificationContent() *NotificationContent { voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag) unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL) - return ¬ificationContent{ + + return &NotificationContent{ template: "create_motion_mail.txt", data: struct { *Decision @@ -188,13 +205,15 @@ type notificationUpdateMotion struct { func NewNotificationUpdateMotion(decision Decision, voter Voter) NotificationMail { notification := notificationUpdateMotion{voter: voter} notification.decision = decision + return ¬ification } -func (n *notificationUpdateMotion) GetNotificationContent() *notificationContent { +func (n *notificationUpdateMotion) GetNotificationContent() *NotificationContent { voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag) unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL) - return ¬ificationContent{ + + return &NotificationContent{ template: "update_motion_mail.txt", data: struct { *Decision @@ -217,11 +236,12 @@ type notificationWithDrawMotion struct { func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) NotificationMail { notification := ¬ificationWithDrawMotion{voter: *voter} notification.decision = *decision + return notification } -func (n *notificationWithDrawMotion) GetNotificationContent() *notificationContent { - return ¬ificationContent{ +func (n *notificationWithDrawMotion) GetNotificationContent() *NotificationContent { + return &NotificationContent{ template: "withdraw_motion_mail.txt", data: struct { *Decision @@ -238,8 +258,8 @@ type RemindVoterNotification struct { decisions []Decision } -func (n *RemindVoterNotification) GetNotificationContent() *notificationContent { - return ¬ificationContent{ +func (n *RemindVoterNotification) GetNotificationContent() *NotificationContent { + return &NotificationContent{ template: "remind_voter_mail.txt", data: struct { Decisions []Decision @@ -266,14 +286,21 @@ type notificationProxyVote struct { justification string } -func NewNotificationProxyVote(decision *Decision, proxy *Voter, voter *Voter, vote *Vote, justification string) NotificationMail { +func NewNotificationProxyVote( + decision *Decision, + proxy *Voter, + voter *Voter, + vote *Vote, + justification string, +) NotificationMail { notification := ¬ificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification} notification.decision = *decision + return notification } -func (n *notificationProxyVote) GetNotificationContent() *notificationContent { - return ¬ificationContent{ +func (n *notificationProxyVote) GetNotificationContent() *NotificationContent { + return &NotificationContent{ template: "proxy_vote_mail.txt", data: struct { Proxy string @@ -298,11 +325,12 @@ type notificationDirectVote struct { func NewNotificationDirectVote(decision *Decision, voter *Voter, vote *Vote) NotificationMail { notification := ¬ificationDirectVote{voter: *voter, vote: *vote} notification.decision = *decision + return notification } -func (n *notificationDirectVote) GetNotificationContent() *notificationContent { - return ¬ificationContent{ +func (n *notificationDirectVote) GetNotificationContent() *NotificationContent { + return &NotificationContent{ template: "direct_vote_mail.txt", data: struct { Vote VoteChoice