├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config ├── config.go └── config_test.go ├── controller ├── controller.go └── controller_test.go ├── go.mod ├── go.sum ├── log ├── log.go └── log_test.go ├── main.go ├── notifier ├── log │ ├── log.go │ └── log_test.go ├── notifier.go └── slack │ ├── parameter.go │ ├── parameter_test.go │ ├── slack.go │ └── slack_test.go ├── source ├── ingress.go ├── ingress_test.go ├── ingress_tls.go ├── ingress_tls_test.go ├── source.go ├── source_test.go ├── tls_endpoint.go └── tls_endpoint_test.go └── synthetics └── datadog ├── datadog.go └── datadog_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: /go/src/github.com/mercari/certificate-expiry-monitor-controller 5 | 6 | golang: &golang 7 | <<: *defaults 8 | docker: 9 | - image: golang:1.19 10 | 11 | jobs: 12 | build: 13 | <<: *golang 14 | steps: 15 | - checkout 16 | - run: 17 | name: Install dependencies 18 | command: | 19 | go mod download 20 | - run: 21 | name: Check compilation 22 | command: | 23 | make build 24 | check: 25 | <<: *golang 26 | steps: 27 | - checkout 28 | - attach_workspace: 29 | at: /go/src/github.com/mercari/certificate-expiry-monitor-controller 30 | - run: 31 | name: Install dpendency tool 32 | command: | 33 | go install golang.org/x/lint/golint 34 | - run: 35 | name: Run go vet 36 | command: | 37 | make vet 38 | - run: 39 | name: Run golint 40 | command: | 41 | make lint 42 | - run: 43 | name: Run test and collect coverages 44 | command: | 45 | make coverage 46 | workflows: 47 | version: 2 48 | build-workflow: 49 | jobs: 50 | - build 51 | - check: 52 | requires: 53 | - build 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## WHAT 2 | (Write what you need) 3 | 4 | ## WHY 5 | (Write the background of this issue) 6 | 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please read the CLA carefully before submitting your contribution to Mercari. 2 | Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. 3 | 4 | https://www.mercari.com/cla/ 5 | 6 | ## WHAT 7 | (Write the change being made with this pull request) 8 | 9 | ## WHY 10 | (Write the motivation why you submit this pull request) 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.2 (2019-01-24) 4 | 5 | ### Enhancement 6 | 7 | - [Added](https://github.com/mercari/certificate-expiry-monitor-controller/pull/3) Support for wildcard certificates. 8 | 9 | ## 0.0.1 (2018-08-30) 10 | 11 | Initial Release 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 2 | 3 | WORKDIR /go/src/github.com/mercari/certificate-expiry-monitor-controller 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . ./ 9 | 10 | RUN CGO_ENABLED=0 GOOS=linux go install -v \ 11 | -ldflags="-w -s" \ 12 | -ldflags "-X main.serviceName=certificate-expiry-monitor-controller" \ 13 | github.com/mercari/certificate-expiry-monitor-controller 14 | 15 | FROM alpine:latest 16 | RUN apk --no-cache add ca-certificates 17 | COPY --from=0 /go/bin/certificate-expiry-monitor-controller /bin/certificate-expiry-monitor-controller 18 | 19 | CMD ["/bin/certificate-expiry-monitor-controller"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Mercari, Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY = certificate-expiry-monitor-controller 2 | PACKAGES = $(shell go list ./...) 3 | 4 | build: 5 | @go build -o $(BINARY) 6 | 7 | test: 8 | @go test -v -parallel=4 $(PACKAGES) 9 | 10 | lint: 11 | @golint $(PACKAGES) 12 | 13 | vet: 14 | @go vet $(PACKAGES) 15 | 16 | coverage: 17 | @go test -v -race -cover -covermode=atomic -coverprofile=coverage.txt $(PACKAGES) 18 | 19 | .PHONY: build container push test lint vet coverage 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certificate Expiry Monitor Controller 2 | 3 | [![CircleCI](https://circleci.com/gh/mercari/certificate-expiry-monitor-controller.svg?style=svg)](https://circleci.com/gh/mercari/certificate-expiry-monitor-controller) 4 | 5 | Certificate Expiry Monitor Controller monitors the expiration of TLS certificates used in Ingress. 6 | 7 | ## Installation 8 | 9 | You can apply to your cluster using the following example. 10 | 11 | ```yaml 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: certificate-expiry-monitor-controller 16 | namespace: kube-system 17 | spec: 18 | replicas: 1 19 | selector: 20 | matchLabels: 21 | app: certificate-expiry-monitor-controller 22 | template: 23 | metadata: 24 | labels: 25 | app: certificate-expiry-monitor-controller 26 | spec: 27 | containers: 28 | - name: certificate-expiry-monitor-controller 29 | image: mercari/certificate-expiry-monitor-controller: 30 | ``` 31 | 32 | Once you apply above, controller will start running inside the cluster and print monitoring results to pod `stderr`. 33 | 34 | ## Usage 35 | 36 | You can set `INTERVAL` and `THRESHOLD` as configuration. Then, the controller monitors the expiration of certificate for each set interval. 37 | If the expiration is expired or the expiration reaches the threshold, the controller sends the alert using the configured notifier. 38 | 39 | ### Notifiers 40 | 41 | In latest version, the contoller supports following notifiers. 42 | 43 | - `slack`: Send information to `SLACK_CHANNEL` in your workspace using `SLACK_TOKEN`. 44 | - `log`: Print information to `stderr`. 45 | 46 | You can select which notifier to send an alert by configuration. 47 | If you not select notifiers, the controller automatically selects `log`. 48 | 49 | ### Configurations 50 | 51 | You can set following configurations by environment variables. 52 | 53 | | ENV | Required | Default | Example | Description | 54 | |--------------------|----------|------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 55 | | `LOG_LEVEL` | false | `INFO` | `DEBUG`, `error` | Configuration of log level for controller's logger. | 56 | | `KUBE_CONFIG_PATH` | false | `~/.kube/config` | `~/.kube/config` | Kubernetes cluster config (If not configured, controller reads local cluster config). | 57 | | `INTERVAL` | false | `12h` | `1m`, `24h`, | Controller verifies expiration of certificate in Ingress at this interval of time. This value must be between `1m` and `24h`. | 58 | | `THRESHOLD` | false | `336h` (2 weeks) | `24h`, `100h`, `336h` | When verifing expiration, controller compares expiration of certificate and `time.Now() - THRESHOLD` to detect issue. This value must be greater than or equal to `24h`. | 59 | | `NOTIFIERS` | false | `log` | `slack,log` | List of alert notifiers. | 60 | | `SLACK_TOKEN` | false | - | - | Slack API token. | 61 | | `SLACK_CHANNEL` | false | - | `random` | Slack channel to send expiration alert (without `#`). | 62 | 63 | ## Synthetics test management 64 | 65 | You can use certificate-expiry-monitor-controller to generate and manage synthetics tests. 66 | It is useful if you want to leverage an external provider synthetics to extend the controller's monitoring capabilities. 67 | **Currently, only Datadog is supported.** 68 | 69 | This functionality is disabled by default and can be toggled on by using the `SYNTHETICS_ENABLED` environment variable. 70 | 71 | Supported features: 72 | 73 | - Adding synthetics tests in Datadog 74 | - Using Ingress endpoint list fetched from Kubernetes API 75 | - Using a predefined environment variable with a list of endpoints to manage 76 | - Deleting synthetics tests in Datadog when not matching existing endpoints 77 | 78 | Synthetics tests have many parts configurable by environment variables: 79 | 80 | - Alert message body 81 | - Check frequency 82 | - Tags 83 | - Default tag 84 | 85 | **Notice: To avoid unwanted destructive behavior with existing synthetics tests, a default tag is used as a safeguard. Only synthetics tests having this default tag will be handled by the controller.** 86 | 87 | ### Configuration 88 | 89 | You can set following configurations for the synthetics test manager by using environment variables. 90 | 91 | | ENV | Required | Default | Example | Description | 92 | |--------------------|----------|------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 93 | | `SYNTHETICS_ENABLED` | false | false | `false`, `true` | Feature-flag to enable synthetics tests management. Disabled by default. 94 | | `DATADOG_API_KEY` | false | - | - | Datadog API key to manage synthetics tests | 95 | | `DATADOG_APPLICATION_KEY` | false | - | - | Datadog application key to manage synthetics tests | 96 | | `SYNTHETICS_ALERT_MESSAGE` | false | "" | `"{{#is_alert}}\n\nCertificate alert, either the expiration data is under XX days or a self-signed certificate.\n\n{{/is_alert}}\n\n @slack-jp-ms-platform-alert"` | Alert message for synthetics tests with failing assertion | 97 | | `SYNTHETICS_CHECK_INTERVAL` | false | `900` | `60`, `300`, `900`, `1800`, `3600`, `21600`, `43200`, `86400`, `604800` | The interval in seconds at which the synthetics test checks will run. Lowest value is 60 seconds (1min) and highest value is 604800 seconds (1 week). | 98 | | `SYNTHETICS_TAGS` | false | "" | `foo:bar`, `"foo:bar, bar:foo"` | List of tags to attribute to synthetics tests, as key:value format string separated by comma. | 99 | | `SYNTHETICS_DEFAULT_TAG` | false | `managed-by-cert-expiry-mon` | `my-control-tag` | Default tag used to control synthetics tests managed by certificate-expiry-monitor-controller. | 100 | | `SYNTHETICS_DEFAULT_LOCATIONS` | false | `"aws:ap-northeast-1"` | `"aws:ap-northeast-1,aws:ap-east-1"` | List of default locations to run synthetic tests from. [Available locations are retrievable here](https://docs.datadoghq.com/api/?lang=bash#get-available-locations) | 101 | | `SYNTHETICS_ADDITIONAL_ENDPOINTS` | false | "" | "example.com,example.com:8443,example2.com:8443" | List of endpoints to add to the synthetics test controller. Useful to monitor services not served by an Ingress. Uses the format `endpoint:port,endpoint2:port2`, port is optional, 443 is implied if not set.| 102 | 103 | ## Future works 104 | 105 | - Support PagerDuty, Datadog and other services as a notifier. 106 | - Support non-default port number. Current implementation only supports `443`. 107 | - Support configurable alert template. 108 | 109 | ## Committers 110 | 111 | Takamasa SAICHI ([@Everysick](https://github.com/Everysick)) 112 | Raphael FRAYSSE ([@lainra](https://github.com/lainra)) 113 | 114 | ## Contribution 115 | 116 | Please read the CLA below carefully before submitting your contribution. 117 | 118 | https://www.mercari.com/cla/ 119 | 120 | ## LICENSE 121 | 122 | Copyright 2018 Mercari, Inc. 123 | 124 | Licensed under the MIT License. 125 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/kelseyhightower/envconfig" 8 | ) 9 | 10 | const ( 11 | // The following values used by validate function. 12 | lowerIntervalMinutes = 1 // INTERVAL must be more than 1 minute 13 | upperIntervalHours = 24 // INTERVAL must be less than 24 hours 14 | lowerThresholdHours = 24 // THRESHOLD must be more than 24 hours 15 | ) 16 | 17 | // Env struct defines configuration of controller that provided by ENV. 18 | type Env struct { 19 | // Original configurations 20 | LogLevel string `envconfig:"LOG_LEVEL" default:"INFO"` 21 | KubeconfigPath string `envconfig:"KUBE_CONFIG_PATH"` 22 | VerifyInterval time.Duration `envconfig:"INTERVAL" default:"12h"` 23 | AlertThreshold time.Duration `envconfig:"THRESHOLD" default:"336h"` 24 | Notifiers []string `envconfig:"NOTIFIERS" default:"log"` 25 | TestManager bool `envconfig:"SYNTHETICS_ENABLED" default:"false"` 26 | 27 | // Configration for Slack 28 | SlackToken string `envconfig:"SLACK_TOKEN"` 29 | SlackChannel string `envconfig:"SLACK_CHANNEL"` 30 | 31 | // Configuration for Datadog 32 | DatadogAPIKey string `envconfig:"DATADOG_API_KEY" default:""` 33 | DatadogAppKey string `envconfig:"DATADOG_APPLICATION_KEY" default:""` 34 | AlertMessage string `envconfig:"SYNTHETICS_ALERT_MESSAGE" default:""` 35 | CheckInterval int `envconfig:"SYNTHETICS_CHECK_INTERVAL" default:"900"` 36 | Tags []string `envconfig:"SYNTHETICS_TAGS" default:""` 37 | DefaultTag string `envconfig:"SYNTHETICS_DEFAULT_TAG" default:"managed-by-cert-expiry-mon"` 38 | DefaultLocations []string `envconfig:"SYNTHETICS_DEFAULT_LOCATIONS" default:"aws:ap-northeast-1"` 39 | AdditionalEndpoints []string `envconfig:"SYNTHETICS_ADDITIONAL_ENDPOINTS" default:""` 40 | } 41 | 42 | // ParseEnv function sets to Env struct and verify it. 43 | // If varify failed, ParseEnv function returns the error immediately. 44 | func (e *Env) ParseEnv() error { 45 | if err := envconfig.Process("", e); err != nil { 46 | return err 47 | } 48 | if err := e.validate(); err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | // validate validates upper and lower limit of configurations. 55 | func (e *Env) validate() error { 56 | validations := []struct { 57 | proposition bool 58 | message string 59 | }{ 60 | { 61 | e.VerifyInterval.Minutes() >= lowerIntervalMinutes, 62 | fmt.Sprintf("INTERVAL must be more than %d minutes", lowerIntervalMinutes), 63 | }, 64 | { 65 | e.VerifyInterval.Hours() <= upperIntervalHours, 66 | fmt.Sprintf("INTERVAL must be less than %d hours", upperIntervalHours), 67 | }, 68 | { 69 | e.AlertThreshold.Hours() >= lowerThresholdHours, 70 | fmt.Sprintf("THRESHOLD must be more than %d hours", lowerThresholdHours), 71 | }, 72 | } 73 | 74 | for _, v := range validations { 75 | if !v.proposition { 76 | return fmt.Errorf(v.message) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestDefaultEnv(t *testing.T) { 9 | var env Env 10 | err := env.ParseEnv() 11 | 12 | if err != nil { 13 | t.Fatal("Failed to parse env that prepared by testcase") 14 | } 15 | if env.VerifyInterval != 12*time.Hour { 16 | t.Fatal("Unexpected default value in INTERVAL") 17 | } 18 | if env.AlertThreshold != 336*time.Hour { 19 | t.Fatal("Unexpected default value in THRESHOLD") 20 | } 21 | if env.LogLevel != "INFO" { 22 | t.Fatal("Unexpected default value in LOG_LEVEL") 23 | } 24 | if env.KubeconfigPath != "" { 25 | t.Fatal("Unexpected default value in KUBE_CONFIG_PATH") 26 | } 27 | if len(env.Notifiers) != 1 || env.Notifiers[0] != "log" { 28 | t.Fatal("Unexpected default value in NOTIFIERS") 29 | } 30 | if env.DatadogAPIKey != "" { 31 | t.Fatal("Unexpected default value in DATADOG_API_KEY") 32 | } 33 | if env.DatadogAppKey != "" { 34 | t.Fatal("Unexpected default value in DATADOG_APPLICATION_KEY") 35 | } 36 | if env.TestManager { 37 | t.Fatal("Unexpected default value in SYNTHETICS_ENABLED") 38 | } 39 | if env.CheckInterval != 900 { 40 | t.Fatal("Unexpected default value in SYNTHETICS_CHECK_INTERVAL") 41 | } 42 | if env.AlertMessage != "" { 43 | t.Fatal("Unexpected default value in SYNTHETICS_ALERT_MESSAGE") 44 | } 45 | if env.Tags != nil { 46 | t.Fatal("Unexpected default value in SYNTHETICS_TAGS") 47 | } 48 | if env.AdditionalEndpoints != nil { 49 | t.Fatal("Unexpected default value in SYNTHETICS_ADDITIONAL_ENDPOINTS") 50 | } 51 | if env.DefaultLocations == nil { 52 | t.Fatal("Unexpected default value in SYNTHETICS_DEFAULT_LOCATIONS") 53 | } 54 | } 55 | 56 | func TestValidate(t *testing.T) { 57 | tests := []struct { 58 | env *Env 59 | expected bool 60 | }{ 61 | struct { 62 | env *Env 63 | expected bool 64 | }{ 65 | env: &Env{VerifyInterval: time.Minute * 1, AlertThreshold: time.Hour * 24}, 66 | expected: true, 67 | }, 68 | struct { 69 | env *Env 70 | expected bool 71 | }{ 72 | env: &Env{VerifyInterval: time.Second * 59, AlertThreshold: time.Hour * 24}, 73 | expected: false, 74 | }, 75 | struct { 76 | env *Env 77 | expected bool 78 | }{ 79 | env: &Env{VerifyInterval: time.Hour * 25, AlertThreshold: time.Hour * 24}, 80 | expected: false, 81 | }, 82 | struct { 83 | env *Env 84 | expected bool 85 | }{ 86 | env: &Env{VerifyInterval: time.Hour * 24, AlertThreshold: time.Hour * 23}, 87 | expected: false, 88 | }, 89 | } 90 | 91 | for _, test := range tests { 92 | result := test.env.validate() 93 | if (result == nil) != test.expected { 94 | if test.expected { 95 | t.Fatalf("Unexpected result error: validation should be success (err: %s)", result) 96 | } else { 97 | t.Fatal("Unexpected result error: validation should be failed") 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "crypto/x509" 5 | "errors" 6 | "time" 7 | 8 | synthetics "github.com/mercari/certificate-expiry-monitor-controller/synthetics/datadog" 9 | 10 | "go.uber.org/zap" 11 | 12 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 13 | "github.com/mercari/certificate-expiry-monitor-controller/source" 14 | 15 | "k8s.io/client-go/kubernetes" 16 | ) 17 | 18 | // onIteration is called when the starting runOnce. Used in testing. 19 | var onIteration = func() {} 20 | 21 | // Controller identifies an instance of a controller. 22 | // It created by calling NewController function. 23 | type Controller struct { 24 | Logger *zap.Logger 25 | Source *source.Source 26 | VerifyInterval time.Duration 27 | AlertThreshold time.Duration 28 | Notifiers []notifier.Notifier 29 | TestManager *synthetics.TestManager 30 | } 31 | 32 | // NewController function validates arguments and 33 | // returns new controller pointer constructed by arguments. 34 | func NewController( 35 | logger *zap.Logger, 36 | clientSet kubernetes.Interface, 37 | interval time.Duration, 38 | threshold time.Duration, 39 | notifiers []notifier.Notifier, 40 | testManager *synthetics.TestManager, 41 | ) (*Controller, error) { 42 | if clientSet == nil { 43 | return nil, errors.New("clientSet must be non nil value") 44 | } 45 | 46 | if logger == nil { 47 | return nil, errors.New("logger must be non nil value") 48 | } 49 | 50 | return &Controller{ 51 | Logger: logger, 52 | Source: source.NewSource(clientSet), 53 | VerifyInterval: interval, 54 | AlertThreshold: threshold, 55 | Notifiers: notifiers, 56 | TestManager: testManager, 57 | }, nil 58 | } 59 | 60 | // Run function starts execution loop that executes runOnce at VerifyInterval. 61 | // If stopCh receives message, Run function terminates execution loop. 62 | func (c *Controller) Run(stopCh chan struct{}) { 63 | c.Logger.Info("Starting controller...") 64 | 65 | for { 66 | onIteration() 67 | currentTime := time.Now() 68 | err := c.runOnce(currentTime) 69 | if err != nil { 70 | c.Logger.Error("Failed to run runOnce: %s", zap.Error(err)) 71 | } 72 | 73 | select { 74 | case <-time.After(c.VerifyInterval): 75 | case <-stopCh: 76 | c.Logger.Info("Terminating controller...") 77 | return 78 | } 79 | } 80 | } 81 | 82 | func (c *Controller) runOnce(currentTime time.Time) error { 83 | ingresses, err := c.Source.Ingresses() 84 | if err != nil { 85 | return err 86 | } 87 | thresholdTime := currentTime.Add(c.AlertThreshold) 88 | 89 | syntheticEndpoints := make(synthetics.SyntheticEndpoints) 90 | 91 | for _, ingress := range ingresses { 92 | for _, tls := range ingress.TLS { 93 | 94 | // Add non overlapping endpoints to a list to manage synthetic tests 95 | for _, tlsEndpoint := range tls.Endpoints { 96 | s, err := synthetics.SyntheticEndpoint{}.FromHostPortStr(tlsEndpoint.Hostname, tlsEndpoint.Port) 97 | 98 | if err != nil { 99 | c.Logger.Warn("Failed to parse synthetic endpoint", zap.Error(err)) 100 | } 101 | 102 | syntheticEndpoints.Add(s) 103 | } 104 | 105 | var certificates []*x509.Certificate 106 | for _, e := range tls.Endpoints { 107 | certificates, err = e.GetCertificates() 108 | if err != nil { 109 | c.Logger.Warn("Detect error when GetCertificates()", zap.String("host", e.Hostname+":"+e.Port), zap.Error(err)) 110 | continue 111 | } 112 | 113 | // Controller assumes that IngressTLS has one certificate chain and all endpoints associated it. 114 | // So, if detect certificate chain the first time, break loop and verify it. 115 | break 116 | } 117 | 118 | if len(certificates) == 0 { 119 | c.Logger.Warn("Remote endpoints has no certificates, but endpoints enabled TLS") 120 | continue 121 | } 122 | 123 | // certs[0] is end-user certificate. 124 | // TODO: able to verify root and intermediate certificate by option 125 | expiration := certificates[0].NotAfter 126 | 127 | opt := notifier.Option{} 128 | if expiration.Before(currentTime) { 129 | // If certificate has been expired. 130 | opt.AlertLevel = notifier.AlertLevelCritical 131 | } else if expiration.Before(thresholdTime) { 132 | // If certificates has been reached the thresholdTime. 133 | opt.AlertLevel = notifier.AlertLevelWarning 134 | } else { 135 | // This expiration has not reached the threshold. 136 | continue 137 | } 138 | 139 | // Send Alert to all notifiers. 140 | for _, notifier := range c.Notifiers { 141 | err := notifier.Alert(expiration, ingress, tls, opt) 142 | 143 | if err != nil { 144 | c.Logger.Warn("Failed to send Alert", zap.Error(err)) 145 | } 146 | } 147 | } 148 | } 149 | 150 | if c.TestManager.Enabled { 151 | // Create managed synthetics tests matching the Ingress endpoint list 152 | c.Logger.Info("Checking if tests need to be created") 153 | 154 | for _, e := range c.TestManager.AdditionalEndpoints { 155 | s, err := (synthetics.SyntheticEndpoint{}).FromString(e) 156 | 157 | if err != nil { 158 | c.Logger.Warn("Failed to parse synthetic endpoint", zap.Error(err)) 159 | } 160 | 161 | syntheticEndpoints.Add(s) 162 | } 163 | 164 | err = c.TestManager.CreateManagedSyntheticsTests(syntheticEndpoints) 165 | if err != nil { 166 | c.Logger.Warn("Failed to create synthetics tests", zap.Error(err)) 167 | } 168 | 169 | c.Logger.Info("Checking if orphaned tests need to be deleted") 170 | 171 | // Delete managed synthetics tests not matching the Ingress endpoint list 172 | err := c.TestManager.DeleteManagedSyntheticsTests(syntheticEndpoints) 173 | if err != nil { 174 | c.Logger.Warn("Failed to delete synthetics tests", zap.Error(err)) 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /controller/controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | "time" 9 | 10 | synthetics "github.com/mercari/certificate-expiry-monitor-controller/synthetics/datadog" 11 | 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | "go.uber.org/zap/zaptest/observer" 15 | 16 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 17 | "github.com/mercari/certificate-expiry-monitor-controller/notifier/log" 18 | "github.com/mercari/certificate-expiry-monitor-controller/source" 19 | 20 | v1 "k8s.io/api/networking/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/kubernetes/fake" 24 | ) 25 | 26 | const ( 27 | dummyURL = "sample.mercari.dummy" 28 | ) 29 | 30 | func TestNewController(t *testing.T) { 31 | type testArg struct { 32 | logger *zap.Logger 33 | clientSet kubernetes.Interface 34 | interval time.Duration 35 | threshold time.Duration 36 | notifiers []notifier.Notifier 37 | testManager *synthetics.TestManager 38 | } 39 | tests := []struct { 40 | arg testArg 41 | success bool 42 | }{ 43 | { 44 | arg: testArg{ 45 | logger: zap.NewNop(), 46 | interval: 10 * time.Hour, 47 | threshold: 48 * time.Hour, 48 | notifiers: []notifier.Notifier{}, 49 | }, 50 | success: false, 51 | }, 52 | { 53 | arg: testArg{ 54 | clientSet: makeTestClientSet(t, []string{dummyURL}), 55 | interval: 10 * time.Hour, 56 | threshold: 48 * time.Hour, 57 | notifiers: []notifier.Notifier{}, 58 | }, 59 | success: false, 60 | }, 61 | { 62 | arg: testArg{ 63 | logger: zap.NewNop(), 64 | clientSet: makeTestClientSet(t, []string{dummyURL}), 65 | interval: 10 * time.Hour, 66 | threshold: 48 * time.Hour, 67 | notifiers: []notifier.Notifier{}, 68 | }, 69 | success: true, 70 | }, 71 | } 72 | 73 | for _, test := range tests { 74 | _, err := NewController(test.arg.logger, test.arg.clientSet, test.arg.interval, test.arg.threshold, test.arg.notifiers, test.arg.testManager) 75 | 76 | if (err == nil) != test.success { 77 | t.Fatalf("Unexpected result with error: %s", err.Error()) 78 | } 79 | } 80 | } 81 | 82 | func TestRun(t *testing.T) { 83 | server := httptest.NewTLSServer(http.NewServeMux()) 84 | defer server.Close() 85 | u, _ := url.Parse(server.URL) 86 | 87 | // Overwrite default port number to test server.URL 88 | source.DefaultPortNumber = u.Port() 89 | 90 | // Observe all logs that printed by notifiers 91 | core, recorded := observer.New(zapcore.InfoLevel) 92 | 93 | stopCh := make(chan struct{}, 1) 94 | interval := 1 * time.Second 95 | threshold := 24 * time.Hour 96 | notifiers := []notifier.Notifier{log.NewNotifier(zap.NewNop())} 97 | clientSet := makeTestClientSet(t, []string{u.Hostname()}) 98 | testManager, err := synthetics.NewTestManager("api_key", "app_key") 99 | testManager.Client = nil 100 | 101 | controller, err := NewController(zap.New(core), clientSet, interval, threshold, notifiers, testManager) 102 | if err != nil { 103 | t.Fatalf("Unexpected falied to initialize controller: %s", err.Error()) 104 | } 105 | 106 | expectedCount := 2 107 | expectedMessage := "onInteration" 108 | 109 | onIteration = func() { 110 | controller.Logger.Info(expectedMessage) 111 | } 112 | 113 | defer func() { 114 | onIteration = func() {} 115 | }() 116 | 117 | go func() { 118 | for i := 0; i < expectedCount; i++ { 119 | <-time.After(interval) 120 | } 121 | close(stopCh) 122 | }() 123 | 124 | controller.Run(stopCh) 125 | 126 | fields := recorded.FilterMessage(expectedMessage) 127 | if fields.Len() != expectedCount { 128 | t.Fatalf("Unexpected count that triggered WARNING alert: %d", fields.Len()) 129 | } 130 | } 131 | 132 | func TestRunOnce(t *testing.T) { 133 | server := httptest.NewTLSServer(http.NewServeMux()) 134 | defer server.Close() 135 | u, _ := url.Parse(server.URL) 136 | 137 | // Prefetch certs to get expiration for testing 138 | expectedCerts, _ := source.NewTLSEndpoint(u.Hostname(), u.Port()).GetCertificates() 139 | expiration := expectedCerts[0].NotAfter 140 | 141 | // Overwrite default port number to test server.URL 142 | source.DefaultPortNumber = u.Port() 143 | 144 | interval := 10 * time.Hour 145 | threshold := 48 * time.Hour 146 | clientSet := makeTestClientSet(t, []string{u.Hostname()}) 147 | testManager, _ := synthetics.NewTestManager("api_key", "app_key") 148 | testManager.Client = nil 149 | 150 | tests := []struct { 151 | arg time.Time 152 | expectedMatchField zap.Field 153 | expectedMatchCount int 154 | }{ 155 | { 156 | // expiration has not reached the threshold. 157 | arg: expiration.Add(-(threshold + threshold)), 158 | expectedMatchField: zap.String("Level", "WARNING"), 159 | expectedMatchCount: 0, 160 | }, 161 | { 162 | // expiration has not reached the threshold. 163 | arg: expiration.Add(-(threshold + threshold)), 164 | expectedMatchField: zap.String("Level", "CRITICAL"), 165 | expectedMatchCount: 0, 166 | }, 167 | { 168 | // expiration reached the threshold. 169 | arg: expiration, 170 | expectedMatchField: zap.String("Level", "WARNING"), 171 | expectedMatchCount: 1, 172 | }, 173 | { 174 | // already expired 175 | arg: expiration.Add(threshold), 176 | expectedMatchField: zap.String("Level", "CRITICAL"), 177 | expectedMatchCount: 1, 178 | }, 179 | } 180 | 181 | for _, test := range tests { 182 | core, recorded := observer.New(zapcore.InfoLevel) 183 | 184 | notifiers := []notifier.Notifier{log.NewNotifier(zap.New(core))} 185 | 186 | controller, err := NewController(zap.NewNop(), clientSet, interval, threshold, notifiers, testManager) 187 | if err != nil { 188 | t.Fatalf("Unexpected falied to initialize controller: %s", err.Error()) 189 | } 190 | 191 | err = controller.runOnce(test.arg) 192 | if err != nil { 193 | t.Fatalf("Unexpected falied to run runOnce: %s", err.Error()) 194 | } 195 | 196 | fields := recorded.FilterField(test.expectedMatchField) 197 | if fields.Len() != test.expectedMatchCount { 198 | t.Fatalf("Not found expected value: { %s: %s }", test.expectedMatchField.Key, test.expectedMatchField.String) 199 | } 200 | } 201 | } 202 | 203 | func makeTestClientSet(t *testing.T, availableHosts []string) kubernetes.Interface { 204 | t.Helper() 205 | 206 | return fake.NewSimpleClientset( 207 | &v1.IngressList{ 208 | Items: []v1.Ingress{ 209 | // case: expected 210 | v1.Ingress{ 211 | ObjectMeta: metav1.ObjectMeta{ 212 | Name: "ingress1", 213 | Namespace: "namespace1", 214 | ClusterName: "clusterName", 215 | }, 216 | Spec: v1.IngressSpec{ 217 | TLS: []v1.IngressTLS{ 218 | { 219 | Hosts: availableHosts, 220 | SecretName: "ingressSecret1", 221 | }, 222 | }, 223 | }, 224 | }, 225 | // case: empty TLS 226 | v1.Ingress{ 227 | ObjectMeta: metav1.ObjectMeta{ 228 | Name: "ingress2", 229 | Namespace: "namespace2", 230 | ClusterName: "clusterName", 231 | }, 232 | Spec: v1.IngressSpec{ 233 | TLS: []v1.IngressTLS{}, 234 | }, 235 | }, 236 | // case: unreachable host 237 | v1.Ingress{ 238 | ObjectMeta: metav1.ObjectMeta{ 239 | Name: "ingress3", 240 | Namespace: "namespace3", 241 | ClusterName: "clusterName", 242 | }, 243 | Spec: v1.IngressSpec{ 244 | TLS: []v1.IngressTLS{ 245 | { 246 | Hosts: []string{dummyURL}, 247 | SecretName: "ingressSecret3", 248 | }, 249 | }, 250 | }, 251 | }, 252 | // case: empty hosts (but TLS enabled) 253 | v1.Ingress{ 254 | ObjectMeta: metav1.ObjectMeta{ 255 | Name: "ingress4", 256 | Namespace: "namespace4", 257 | ClusterName: "clusterName", 258 | }, 259 | Spec: v1.IngressSpec{ 260 | TLS: []v1.IngressTLS{ 261 | { 262 | Hosts: []string{}, 263 | SecretName: "ingressSecret4", 264 | }, 265 | }, 266 | }, 267 | }, 268 | }, 269 | }, 270 | ) 271 | } 272 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mercari/certificate-expiry-monitor-controller 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/kelseyhightower/envconfig v1.4.0 7 | github.com/nlopes/slack v0.3.0 8 | github.com/zorkian/go-datadog-api v2.25.0+incompatible 9 | go.uber.org/ratelimit v0.1.0 10 | go.uber.org/zap v1.13.0 11 | k8s.io/api v0.23.10 12 | k8s.io/apimachinery v0.23.10 13 | k8s.io/client-go v0.23.10 14 | ) 15 | 16 | require ( 17 | cloud.google.com/go v0.81.0 // indirect 18 | github.com/BurntSushi/toml v0.3.1 // indirect 19 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 22 | github.com/go-logr/logr v1.2.0 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/golang/protobuf v1.5.2 // indirect 25 | github.com/google/go-cmp v0.5.5 // indirect 26 | github.com/google/gofuzz v1.1.0 // indirect 27 | github.com/googleapis/gnostic v0.5.5 // indirect 28 | github.com/gorilla/websocket v1.4.2 // indirect 29 | github.com/imdario/mergo v0.3.8 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/pkg/errors v0.9.1 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | go.uber.org/atomic v1.5.1 // indirect 37 | go.uber.org/multierr v1.4.0 // indirect 38 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect 39 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect 40 | golang.org/x/mod v0.4.2 // indirect 41 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 42 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect 43 | golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect 44 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect 45 | golang.org/x/text v0.3.7 // indirect 46 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 47 | golang.org/x/tools v0.1.5 // indirect 48 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 49 | google.golang.org/appengine v1.6.7 // indirect 50 | google.golang.org/protobuf v1.27.1 // indirect 51 | gopkg.in/inf.v0 v0.9.1 // indirect 52 | gopkg.in/yaml.v2 v2.4.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 54 | honnef.co/go/tools v0.0.1-2020.1.4 // indirect 55 | k8s.io/klog/v2 v2.30.0 // indirect 56 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect 57 | k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect 58 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect 59 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 60 | sigs.k8s.io/yaml v1.2.0 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= 21 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 22 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 23 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 24 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 25 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 26 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 27 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 28 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 29 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 41 | github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= 42 | github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= 43 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= 44 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 45 | github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 46 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= 47 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 48 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 49 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 50 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 51 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 52 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 53 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 54 | github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= 55 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 56 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 57 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 58 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 59 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 60 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 61 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 62 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 63 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 64 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 65 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 66 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 67 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 68 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 69 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 70 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 71 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 72 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 73 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 74 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 75 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 76 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 77 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 78 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 79 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 80 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 81 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 82 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 83 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 84 | github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= 85 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 86 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 87 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 88 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 89 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 90 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 91 | github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= 92 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 93 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 94 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 95 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 96 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 97 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 98 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 99 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 100 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 101 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 102 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 103 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 104 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 105 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 106 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 107 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 108 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 109 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 110 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 111 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 112 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 113 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 114 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 115 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 116 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 117 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 118 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 119 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 120 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 121 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 122 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 123 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 124 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 125 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 126 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 127 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 128 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 129 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 130 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 131 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 132 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 133 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 134 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 135 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 136 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 144 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 145 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 146 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 147 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 148 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 149 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 150 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 151 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 152 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 153 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 154 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 155 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 156 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 157 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 158 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 159 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 160 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 161 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 162 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 163 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 164 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 165 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 166 | github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= 167 | github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= 168 | github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= 169 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 170 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 171 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 172 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 173 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 174 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 175 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 176 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 177 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 178 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 179 | github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= 180 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 181 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 182 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 183 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 184 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 185 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 186 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 187 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 188 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 189 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 190 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 191 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 192 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 193 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 194 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 195 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 196 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 h1:MNApn+Z+fIT4NPZopPfCc1obT6aY3SVM6DOctz1A9ZU= 197 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= 198 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 199 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 200 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 201 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 202 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 203 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 204 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 205 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 206 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 207 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 208 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 209 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 210 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 211 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 212 | github.com/nlopes/slack v0.3.0 h1:jCxvaS8wC4Bb1jnbqZMjCDkOOgy4spvQWcrw/TF0L0E= 213 | github.com/nlopes/slack v0.3.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= 214 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 215 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 216 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 217 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 218 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 219 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 220 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 221 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 222 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 223 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 224 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 225 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 226 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 227 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 228 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 229 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 230 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 231 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 232 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 233 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 234 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 235 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 236 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 237 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 238 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 239 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 240 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 241 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 242 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 243 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 244 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 245 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 246 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 247 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 248 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 249 | github.com/zorkian/go-datadog-api v2.25.0+incompatible h1:6uTEIky3RbhdqlZCdeGYBtIb+quwNwMdbYLADl+cASU= 250 | github.com/zorkian/go-datadog-api v2.25.0+incompatible/go.mod h1:PkXwHX9CUQa/FpB9ZwAD45N1uhCW4MT/Wj7m36PbKss= 251 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 252 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 253 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 254 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 255 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 256 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 257 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 258 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 259 | go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM= 260 | go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 261 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 262 | go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E= 263 | go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 264 | go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw= 265 | go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= 266 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 267 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 268 | go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= 269 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 270 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 271 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 272 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 273 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 274 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 275 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 276 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 277 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 278 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 279 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 280 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 281 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 282 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 283 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 284 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 285 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 286 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 287 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 288 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 289 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 290 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 291 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 292 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 293 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 294 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 295 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 296 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 297 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 298 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 299 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= 300 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 301 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 302 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 303 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 304 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 305 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 306 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 307 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 308 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 309 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 310 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 311 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 312 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 313 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 314 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 315 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 316 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 317 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 318 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 319 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 320 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 321 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 322 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 323 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 324 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 325 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 326 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 327 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 328 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 329 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 330 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 331 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 332 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 333 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 334 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 335 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 336 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 337 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 338 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 339 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 340 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 341 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 342 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 343 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 344 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 345 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 346 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 347 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 348 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 349 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 350 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= 351 | golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 352 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 353 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 354 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 355 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 356 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 357 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 358 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 359 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 360 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 361 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 362 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 363 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= 364 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 365 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 366 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 367 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 368 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 369 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 370 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 371 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 372 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 373 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 374 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 375 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 376 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 377 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 378 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 379 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 411 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 419 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 420 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 421 | golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= 422 | golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 423 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 424 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= 425 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 426 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 427 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 428 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 429 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 430 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 431 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 432 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 433 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 434 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 435 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 436 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 437 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 438 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 439 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= 440 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 441 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 442 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 443 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 444 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 445 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 446 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 447 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 448 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 449 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 450 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 451 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 452 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 453 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 454 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 455 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 456 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 457 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 458 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 459 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 460 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 461 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 462 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 463 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 464 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 465 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 466 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 467 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 468 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 469 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 470 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 471 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 472 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 473 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 474 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 475 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 476 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 477 | golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 478 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 479 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 480 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 481 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 482 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 483 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 484 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 485 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 486 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 487 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 488 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 489 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 490 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 491 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 492 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 493 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 494 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 495 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 496 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 497 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 498 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 499 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 500 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 501 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 502 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 503 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 504 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 505 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 506 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 507 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 508 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 509 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 510 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 511 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 512 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 513 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 514 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 515 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 516 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 517 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 518 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 519 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 520 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 521 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 522 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 523 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 524 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 525 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 526 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 527 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 528 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 529 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 530 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 531 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 532 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 533 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 534 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 535 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 536 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 537 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 538 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 539 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 540 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 541 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 542 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 543 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 544 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 545 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 546 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 547 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 548 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 549 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 550 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 551 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 552 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 553 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 554 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 555 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 556 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 557 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 558 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 559 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 560 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 561 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 562 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 563 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 564 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 565 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 566 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 567 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 568 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 569 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 570 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 571 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 572 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 573 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 574 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 575 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 576 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 577 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 578 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 579 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 580 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 581 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 582 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 583 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 584 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 585 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 586 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 587 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 588 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 589 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 590 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 591 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 592 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 593 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 594 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 595 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 596 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 597 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 598 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 599 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 600 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 601 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 602 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 603 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 604 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 605 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 606 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 607 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 608 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 609 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 610 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 611 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 612 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 613 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 614 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 615 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 616 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 617 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 618 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 619 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 620 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 621 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 622 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 623 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 624 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 625 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 626 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 627 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 628 | honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= 629 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 630 | k8s.io/api v0.23.10 h1:lbE62dyusvPnPUD5plS9X3hJQ4EWUt8iG69AigC6skE= 631 | k8s.io/api v0.23.10/go.mod h1:+vpqBLTniW6bWQN7M3g3fttf3wQduJuRRmXGSb34Gas= 632 | k8s.io/apimachinery v0.23.10 h1:ZZTDUh8kcKvpjptg/zOii7paedymrOIO9WOOOfZScVk= 633 | k8s.io/apimachinery v0.23.10/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= 634 | k8s.io/client-go v0.23.10 h1:iDTADqhGG4VBHFGWVQEwRvG3oG8viD2WtFgp36AZ8Dw= 635 | k8s.io/client-go v0.23.10/go.mod h1:+LdgZowNVtLC2/cGLEep+XuVa9jD0hjmxBwH/v+CZqM= 636 | k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 637 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 638 | k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 639 | k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= 640 | k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 641 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= 642 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= 643 | k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 644 | k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= 645 | k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 646 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 647 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 648 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 649 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= 650 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= 651 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 652 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= 653 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= 654 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 655 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 656 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | // NewLogger creates new logger that defined as zap.Logger 12 | func NewLogger(levelStr string) (*zap.Logger, error) { 13 | level, err := parseLogLevel(levelStr) 14 | if err != nil { 15 | return nil, fmt.Errorf("Failed to parse log level: %s", err.Error()) 16 | } 17 | 18 | config := zap.Config{ 19 | Level: zap.NewAtomicLevelAt(level), 20 | DisableStacktrace: true, 21 | Development: false, 22 | Encoding: "json", 23 | OutputPaths: []string{"stdout"}, 24 | ErrorOutputPaths: []string{"stderr"}, 25 | Sampling: &zap.SamplingConfig{ 26 | Initial: 100, 27 | Thereafter: 100, 28 | }, 29 | EncoderConfig: zapcore.EncoderConfig{ 30 | TimeKey: "ts", 31 | LevelKey: "level", 32 | NameKey: "logger", 33 | CallerKey: "caller", 34 | MessageKey: "msg", 35 | StacktraceKey: "stacktrace", 36 | LineEnding: zapcore.DefaultLineEnding, 37 | EncodeLevel: zapcore.LowercaseLevelEncoder, 38 | EncodeTime: zapcore.EpochTimeEncoder, 39 | EncodeDuration: zapcore.SecondsDurationEncoder, 40 | EncodeCaller: zapcore.ShortCallerEncoder, 41 | }, 42 | } 43 | 44 | return config.Build() 45 | } 46 | 47 | func parseLogLevel(levelStr string) (zapcore.Level, error) { 48 | var level zapcore.Level 49 | 50 | switch strings.ToUpper(levelStr) { 51 | case zapcore.DebugLevel.CapitalString(): 52 | level = zapcore.DebugLevel 53 | case zapcore.InfoLevel.CapitalString(): 54 | level = zapcore.InfoLevel 55 | case zapcore.WarnLevel.CapitalString(): 56 | level = zapcore.WarnLevel 57 | case zapcore.ErrorLevel.CapitalString(): 58 | level = zapcore.ErrorLevel 59 | default: 60 | return level, fmt.Errorf("Undefined log level: %s", levelStr) 61 | } 62 | 63 | return level, nil 64 | } 65 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/zap/zapcore" 7 | ) 8 | 9 | func TestNewLogger(t *testing.T) { 10 | if _, err := NewLogger("INFO"); err != nil { 11 | t.Fatalf("Unexpected fail: %s", err.Error()) 12 | } 13 | 14 | if _, err := NewLogger("DUMMY"); err == nil { 15 | t.Fatal("Unexpected success") 16 | } 17 | } 18 | 19 | func TestParseLogLevel(t *testing.T) { 20 | tests := []struct { 21 | arg string 22 | expectedValue zapcore.Level 23 | success bool 24 | }{ 25 | { 26 | arg: "info", 27 | expectedValue: zapcore.InfoLevel, 28 | success: true, 29 | }, 30 | { 31 | arg: "info", 32 | expectedValue: zapcore.InfoLevel, 33 | success: true, 34 | }, 35 | { 36 | arg: "DUMMY", 37 | success: false, 38 | }, 39 | } 40 | 41 | for _, test := range tests { 42 | actual, err := parseLogLevel(test.arg) 43 | 44 | if (err == nil) != test.success { 45 | if test.success { 46 | t.Fatalf("Unexpected fail by error: %s", err.Error()) 47 | } else { 48 | t.Fatalf("Unexpected success") 49 | } 50 | } 51 | 52 | if actual != test.expectedValue { 53 | t.Fatalf("Unexpected result value: %d", actual) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/mercari/certificate-expiry-monitor-controller/config" 10 | "github.com/mercari/certificate-expiry-monitor-controller/controller" 11 | logging "github.com/mercari/certificate-expiry-monitor-controller/log" 12 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 13 | "github.com/mercari/certificate-expiry-monitor-controller/notifier/log" 14 | "github.com/mercari/certificate-expiry-monitor-controller/notifier/slack" 15 | synthetics "github.com/mercari/certificate-expiry-monitor-controller/synthetics/datadog" 16 | 17 | "k8s.io/client-go/kubernetes" 18 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 19 | "k8s.io/client-go/rest" 20 | "k8s.io/client-go/tools/clientcmd" 21 | ) 22 | 23 | func main() { 24 | os.Exit(runMain()) 25 | } 26 | 27 | func runMain() int { 28 | // Parse configurations from environment variables 29 | var env config.Env 30 | if err := env.ParseEnv(); err != nil { 31 | fmt.Fprintf(os.Stderr, "[ERROR] Failed to parse environement variables: %s\n", err.Error()) 32 | return 1 33 | } 34 | 35 | // Setup clientSet from configuration. 36 | clientSet, err := newClientSet(env.KubeconfigPath) 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "[ERROR] Failed to create clientSet: %s\n", err.Error()) 39 | return 1 40 | } 41 | 42 | // Setup notifiers from configuration. 43 | // If user specify unsupported notifier name, program returns exit code `1`. 44 | notifiers := make([]notifier.Notifier, len(env.Notifiers)) 45 | for i, name := range env.Notifiers { 46 | switch name { 47 | case slack.String(): 48 | sl, err := slack.NewNotifier(env.SlackToken, env.SlackChannel) 49 | if err != nil { 50 | fmt.Fprintf(os.Stderr, "[ERROR] Failed to create slack notifier: %s\n", err.Error()) 51 | return 1 52 | } 53 | 54 | notifiers[i] = sl 55 | case log.String(): 56 | logger, err := logging.NewLogger(log.AlertLogLevel()) 57 | if err != nil { 58 | fmt.Fprintf(os.Stderr, "[ERROR] Failed to create log notifier: %s\n", err.Error()) 59 | return 1 60 | } 61 | 62 | notifiers[i] = log.NewNotifier(logger) 63 | default: 64 | fmt.Fprintf(os.Stderr, "[ERROR] Unexpected notifier name: %s\n", name) 65 | return 1 66 | } 67 | } 68 | 69 | // Setup logger that wrapped zap.Logger to use common settings. 70 | logger, err := logging.NewLogger(env.LogLevel) 71 | if err != nil { 72 | fmt.Fprintf(os.Stderr, "[ERROR] Failed to create logger: %s\n", err.Error()) 73 | return 1 74 | } 75 | 76 | // Create a new synthetics testManager instance 77 | testManager := &synthetics.TestManager{} 78 | if env.TestManager { 79 | testManager, err = synthetics.NewTestManager(env.DatadogAPIKey, env.DatadogAppKey) 80 | if err != nil { 81 | fmt.Fprintf(os.Stderr, "[ERROR] Failed to create testManager: %s\n", err.Error()) 82 | return 1 83 | } 84 | testManager.Logger = logger 85 | testManager.CheckInterval = env.CheckInterval 86 | testManager.AlertMessage = env.AlertMessage 87 | testManager.Tags = env.Tags 88 | testManager.DefaultTag = env.DefaultTag 89 | testManager.AdditionalEndpoints = env.AdditionalEndpoints 90 | testManager.DefaultLocations = env.DefaultLocations 91 | 92 | // Set control flag to prevent running the synthetics logic in the controller when feature-gated 93 | testManager.Enabled = true 94 | } 95 | 96 | // Create new controller instance. 97 | controller, err := controller.NewController(logger, clientSet, env.VerifyInterval, env.AlertThreshold, notifiers, testManager) 98 | if err != nil { 99 | fmt.Fprintf(os.Stderr, "[ERROR] Failed to create controller: %s\n", err.Error()) 100 | return 1 101 | } 102 | 103 | // When controller receives SIGINT or SIGTERM, 104 | // handleSignal goroutine triggers stopCh to terminate controller. 105 | stopCh := make(chan struct{}, 1) 106 | go handleSignal(stopCh) 107 | 108 | controller.Run(stopCh) 109 | 110 | return 0 111 | } 112 | 113 | // Create new Kubernetes's clientSet. 114 | // When configured env.KubeconfigPath, read config from env.KubeconfigPath. 115 | // When not configured env.KubeconfigPath, read internal cluster config. 116 | func newClientSet(kubeconfigPath string) (*kubernetes.Clientset, error) { 117 | var err error 118 | var config *rest.Config 119 | 120 | if kubeconfigPath == "" { 121 | config, err = rest.InClusterConfig() 122 | } else { 123 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) 124 | } 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | clientSet, err := kubernetes.NewForConfig(config) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | return clientSet, nil 135 | } 136 | 137 | // Handling syscall.SIGTERM and syscall.SIGINT 138 | // When trap those, function send message to stopCh 139 | func handleSignal(stopCh chan struct{}) { 140 | signalCh := make(chan os.Signal, 1) 141 | 142 | signal.Notify(signalCh, syscall.SIGTERM) 143 | signal.Notify(signalCh, syscall.SIGINT) 144 | 145 | <-signalCh 146 | close(stopCh) 147 | } 148 | -------------------------------------------------------------------------------- /notifier/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | 10 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 11 | "github.com/mercari/certificate-expiry-monitor-controller/source" 12 | ) 13 | 14 | const ( 15 | // alertLogLevel used when create new log notifier 16 | alertLogLevel = "ERROR" 17 | 18 | // notifierName used by pattern match when parse interpret options. 19 | notifierName = "log" 20 | ) 21 | 22 | // Log struct implements notifier.Notifier interface. 23 | // Log struct output alert information using application logger. 24 | type Log struct { 25 | Logger *zap.Logger 26 | } 27 | 28 | // NewNotifier function returns new instance of Log. 29 | func NewNotifier(logger *zap.Logger) notifier.Notifier { 30 | return &Log{ 31 | Logger: logger, 32 | } 33 | } 34 | 35 | // String function used by pattern match when parse interpret options. 36 | func String() string { 37 | return notifierName 38 | } 39 | 40 | // AlertLogLevel called when create new log notifier 41 | func AlertLogLevel() string { 42 | return alertLogLevel 43 | } 44 | 45 | // Alert defined by notifier.Notifier interface. 46 | // This function create and print fields using log package. 47 | func (log *Log) Alert(expiration time.Time, ingress *source.Ingress, tls *source.IngressTLS, opt notifier.Option) error { 48 | fields := loggingFields(ingress.ClusterName, ingress.Namespace, ingress.Name, tls.SecretName, expiration, tls.Endpoints, opt.AlertLevel) 49 | log.Logger.Error("ALERT", fields...) 50 | return nil 51 | } 52 | 53 | func loggingFields( 54 | cluster string, 55 | namespace string, 56 | name string, 57 | secret string, 58 | expiration time.Time, 59 | endpoints []*source.TLSEndpoint, 60 | alertLevel notifier.AlertLevel, 61 | ) []zapcore.Field { 62 | 63 | var level string 64 | switch alertLevel { 65 | case notifier.AlertLevelWarning: 66 | level = "WARNING" 67 | case notifier.AlertLevelCritical: 68 | level = "CRITICAL" 69 | } 70 | 71 | hosts := make([]string, len(endpoints)) 72 | for i, e := range endpoints { 73 | hosts[i] = e.Hostname + ":" + e.Port 74 | } 75 | 76 | return []zapcore.Field{ 77 | zap.String("Level", level), 78 | zap.String("ClusterName", cluster), 79 | zap.String("Namespace", namespace), 80 | zap.String("Ingress", name), 81 | zap.String("TLS Secret name", secret), 82 | zap.String("Expiration", expiration.Format(time.RFC822)), 83 | zap.String("Hosts", strings.Join(hosts, ",")), 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /notifier/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | "go.uber.org/zap/zaptest/observer" 10 | 11 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 12 | "github.com/mercari/certificate-expiry-monitor-controller/source" 13 | ) 14 | 15 | func TestAlert(t *testing.T) { 16 | type TestArg struct { 17 | expiration time.Time 18 | ingress *source.Ingress 19 | tls *source.IngressTLS 20 | opt notifier.Option 21 | } 22 | 23 | type TestCase struct { 24 | args TestArg 25 | expectedMatchField zap.Field 26 | expectedMatchCount int 27 | } 28 | 29 | tests := []TestCase{ 30 | { 31 | args: TestArg{ 32 | expiration: time.Now(), 33 | ingress: makeTestIngress(t), 34 | tls: makeTestIngressTLS(t), 35 | opt: notifier.Option{AlertLevel: notifier.AlertLevelWarning}, 36 | }, 37 | expectedMatchField: zap.String("Level", "WARNING"), 38 | expectedMatchCount: 1, 39 | }, 40 | { 41 | args: TestArg{ 42 | expiration: time.Now(), 43 | ingress: makeTestIngress(t), 44 | tls: makeTestIngressTLS(t), 45 | opt: notifier.Option{AlertLevel: notifier.AlertLevelCritical}, 46 | }, 47 | expectedMatchField: zap.String("Level", "CRITICAL"), 48 | expectedMatchCount: 1, 49 | }, 50 | } 51 | 52 | for _, test := range tests { 53 | core, recorded := observer.New(zapcore.InfoLevel) 54 | l := NewNotifier(zap.New(core)) 55 | l.Alert(test.args.expiration, test.args.ingress, test.args.tls, test.args.opt) 56 | 57 | fields := recorded.FilterField(test.expectedMatchField) 58 | if fields.Len() != test.expectedMatchCount { 59 | t.Fatalf("Not found expected value: { %s: %s }", test.expectedMatchField.Key, test.expectedMatchField.String) 60 | } 61 | } 62 | } 63 | 64 | func makeTestIngress(t *testing.T) *source.Ingress { 65 | t.Helper() 66 | return &source.Ingress{ 67 | ClusterName: "DummyClusterName", 68 | Namespace: "DummyNamespace", 69 | Name: "DummyName", 70 | TLS: []*source.IngressTLS{}, 71 | } 72 | } 73 | 74 | func makeTestIngressTLS(t *testing.T) *source.IngressTLS { 75 | t.Helper() 76 | return &source.IngressTLS{ 77 | Endpoints: []*source.TLSEndpoint{ 78 | source.NewTLSEndpoint("host01.example.com", ""), 79 | source.NewTLSEndpoint("host02.example.com", ""), 80 | }, 81 | SecretName: "DummySecretName", 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /notifier/notifier.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mercari/certificate-expiry-monitor-controller/source" 7 | ) 8 | 9 | // AlertLevel expresses notification level when uses in Alert() function 10 | // notification level hierarky : (high) CRITICAL > WARNING (low) 11 | type AlertLevel int 12 | 13 | const ( 14 | // AlertLevelWarning express that notification level is `WARNING`. 15 | AlertLevelWarning AlertLevel = iota 16 | // AlertLevelCritical express that notification level is `CRITICAL`. 17 | AlertLevelCritical 18 | ) 19 | 20 | // Option struct provides configration about notification. 21 | type Option struct { 22 | AlertLevel AlertLevel 23 | } 24 | 25 | // Notifier interface expresses the notification services that able to send Alert. 26 | // If controller triggers Alert, Notifier send details about certificate's expirarion to own service. 27 | type Notifier interface { 28 | Alert(time.Time, *source.Ingress, *source.IngressTLS, Option) error 29 | } 30 | -------------------------------------------------------------------------------- /notifier/slack/parameter.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | libSlack "github.com/nlopes/slack" 9 | 10 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 11 | "github.com/mercari/certificate-expiry-monitor-controller/source" 12 | ) 13 | 14 | // newPostParameters creates params to pass PostMessage function. 15 | func newPostParameters(expiration time.Time, ingress *source.Ingress, tls *source.IngressTLS, alertLevel notifier.AlertLevel) libSlack.PostMessageParameters { 16 | var color, preText string 17 | 18 | switch alertLevel { 19 | case notifier.AlertLevelCritical: 20 | color = "danger" 21 | days := int64(time.Since(expiration).Hours() / 24) 22 | preText = fmt.Sprintf("[CRITICAL] TLS certificate already expired at %d days ago", days) 23 | case notifier.AlertLevelWarning: 24 | color = "warning" 25 | days := int64(time.Until(expiration).Hours() / 24) 26 | preText = fmt.Sprintf("[WARNING] TLS certificate will expire within %d days", days) 27 | } 28 | 29 | return libSlack.PostMessageParameters{ 30 | Username: "Certificate Expiry Monitor", 31 | Attachments: []libSlack.Attachment{ 32 | libSlack.Attachment{ 33 | Color: color, 34 | Pretext: preText, 35 | Fields: newAttachmentFields(ingress.ClusterName, ingress.Namespace, ingress.Name, tls.SecretName, expiration, tls.Endpoints), 36 | }, 37 | }, 38 | } 39 | } 40 | 41 | // newAttachmentFields creates attachment filed slice that may included in PostParameter. 42 | func newAttachmentFields(cluster string, namespace string, name string, secret string, expiration time.Time, endpoints []*source.TLSEndpoint) []libSlack.AttachmentField { 43 | hosts := make([]string, len(endpoints)) 44 | for i, e := range endpoints { 45 | hosts[i] = e.Hostname + ":" + e.Port 46 | } 47 | 48 | return []libSlack.AttachmentField{ 49 | libSlack.AttachmentField{Title: "Cluster", Value: cluster}, 50 | libSlack.AttachmentField{Title: "Namespace", Value: namespace}, 51 | libSlack.AttachmentField{Title: "Ingress", Value: name}, 52 | libSlack.AttachmentField{Title: "TLS secret name", Value: secret}, 53 | libSlack.AttachmentField{Title: "Expiration", Value: expiration.Format(time.RFC822)}, 54 | libSlack.AttachmentField{Title: "Hosts", Value: strings.Join(hosts, "\n")}, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /notifier/slack/parameter_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 10 | "github.com/mercari/certificate-expiry-monitor-controller/source" 11 | ) 12 | 13 | func TestNewPostParameters(t *testing.T) { 14 | expectedDays := 5 15 | 16 | type TestArg struct { 17 | expiration time.Time 18 | ingress *source.Ingress 19 | tls *source.IngressTLS 20 | alertLevel notifier.AlertLevel 21 | } 22 | 23 | type TestExpect struct { 24 | color string 25 | days int 26 | subPreText string 27 | } 28 | 29 | type TestCase struct { 30 | args TestArg 31 | expected TestExpect 32 | } 33 | 34 | tests := []TestCase{ 35 | { 36 | args: TestArg{ 37 | // To consider effect of time lapse: `.Add(time.Hour * 12)` 38 | expiration: time.Now().AddDate(0, 0, expectedDays).Add(time.Hour * 12), 39 | ingress: makeTestIngress(t), 40 | tls: makeTestIngressTLS(t), 41 | alertLevel: notifier.AlertLevelWarning, 42 | }, 43 | expected: TestExpect{ 44 | color: "warning", 45 | days: 5, 46 | subPreText: "[WARNING]", 47 | }, 48 | }, 49 | { 50 | args: TestArg{ 51 | expiration: time.Now().AddDate(0, 0, -expectedDays), 52 | ingress: makeTestIngress(t), 53 | tls: makeTestIngressTLS(t), 54 | alertLevel: notifier.AlertLevelCritical, 55 | }, 56 | expected: TestExpect{ 57 | color: "danger", 58 | days: 5, 59 | subPreText: "[CRITICAL]", 60 | }, 61 | }, 62 | } 63 | 64 | for _, test := range tests { 65 | actual := newPostParameters(test.args.expiration, test.args.ingress, test.args.tls, test.args.alertLevel) 66 | actualAttachment := actual.Attachments[0] 67 | 68 | if !strings.Contains(actualAttachment.Pretext, test.expected.subPreText) { 69 | t.Fatalf("Pretext not includes %s: %s", test.expected.subPreText, actualAttachment.Pretext) 70 | } 71 | 72 | if actualAttachment.Color != test.expected.color { 73 | t.Fatalf("Unexpected Alert color %s, expected %s", actualAttachment.Color, test.expected.color) 74 | } 75 | 76 | if !strings.Contains(actualAttachment.Pretext, strconv.Itoa(test.expected.days)) { 77 | t.Fatalf("Pretext not includes expected days %d: %s", test.expected.days, actualAttachment.Pretext) 78 | } 79 | } 80 | } 81 | 82 | func TestNewAttachmentFields(t *testing.T) { 83 | expectedFieldCount := 6 84 | 85 | tests := []struct { 86 | cluster string 87 | namespace string 88 | name string 89 | secret string 90 | expiration time.Time 91 | endpoints []*source.TLSEndpoint 92 | }{ 93 | { 94 | cluster: "dummyCluster", 95 | namespace: "dummyNamespace", 96 | name: "dummyName", 97 | secret: "dummySecret", 98 | expiration: time.Now(), 99 | endpoints: []*source.TLSEndpoint{}, 100 | }, 101 | } 102 | 103 | for _, test := range tests { 104 | actual := newAttachmentFields(test.cluster, test.namespace, test.name, test.secret, test.expiration, test.endpoints) 105 | 106 | if len(actual) != expectedFieldCount { 107 | t.Fatalf("Unexpected number of fields: %d", len(actual)) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /notifier/slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | libSlack "github.com/nlopes/slack" 8 | "go.uber.org/ratelimit" 9 | 10 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 11 | "github.com/mercari/certificate-expiry-monitor-controller/source" 12 | ) 13 | 14 | const ( 15 | // The Slack API has restriction on sending messages. So, notifier send once per second 16 | // See also: https://api.slack.com/docs/rate-limits 17 | sendPerSecond = 1 18 | 19 | // notifierName used by pattern match when parse interpret options. 20 | notifierName = "slack" 21 | ) 22 | 23 | // API interface defines slack's API behavior. 24 | // API interface defined to wrap the library: github.com/nlopes/slack 25 | type API interface { 26 | PostMessage(string, string, libSlack.PostMessageParameters) (string, string, error) 27 | } 28 | 29 | // Slack struct implements notifier.Notifier interface. 30 | // Slack struct sends alert over RESTful API. 31 | type Slack struct { 32 | APIClient API 33 | ChannelName string 34 | RateLimiter ratelimit.Limiter 35 | } 36 | 37 | // NewNotifier function returns new instance of Slack. 38 | // The destination channel is fixed when initialize. 39 | func NewNotifier(token string, channel string) (notifier.Notifier, error) { 40 | if token == "" { 41 | return nil, errors.New("token is missing") 42 | } 43 | 44 | if channel == "" { 45 | return nil, errors.New("channel is missing") 46 | } 47 | 48 | return &Slack{ 49 | APIClient: libSlack.New(token), 50 | ChannelName: channel, 51 | RateLimiter: ratelimit.New(sendPerSecond), 52 | }, nil 53 | } 54 | 55 | // String function used by pattern match when parse interpret options. 56 | func String() string { 57 | return notifierName 58 | } 59 | 60 | // Alert defined by notifier.Notifier interface. 61 | // This implementation post message that includes infromation about ingress and TLS and those deadline. 62 | func (s *Slack) Alert(expiration time.Time, ingress *source.Ingress, tls *source.IngressTLS, opt notifier.Option) error { 63 | params := newPostParameters(expiration, ingress, tls, opt.AlertLevel) 64 | return s.postWithRateLimiter(s.ChannelName, "", params) 65 | } 66 | 67 | func (s *Slack) postWithRateLimiter(channel string, message string, params libSlack.PostMessageParameters) error { 68 | s.RateLimiter.Take() 69 | _, _, err := s.APIClient.PostMessage(channel, message, params) 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /notifier/slack/slack_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | libSlack "github.com/nlopes/slack" 9 | "go.uber.org/ratelimit" 10 | 11 | "github.com/mercari/certificate-expiry-monitor-controller/notifier" 12 | "github.com/mercari/certificate-expiry-monitor-controller/source" 13 | ) 14 | 15 | const ( 16 | stubClientChannelName = "random" 17 | dummyToken = "dummy_token" 18 | ) 19 | 20 | type fakeClient struct { 21 | Token string 22 | } 23 | 24 | func (client *fakeClient) PostMessage(channel string, message string, params libSlack.PostMessageParameters) (string, string, error) { 25 | if channel != stubClientChannelName { 26 | return "", "", errors.New("Unexpected channel name") 27 | } 28 | return "", "", nil 29 | } 30 | 31 | func TestNewNotifier(t *testing.T) { 32 | tests := []struct { 33 | arg struct { 34 | token string 35 | channel string 36 | } 37 | success bool 38 | }{ 39 | { 40 | arg: struct { 41 | token string 42 | channel string 43 | }{ 44 | token: "token", 45 | channel: "channel", 46 | }, 47 | success: true, 48 | }, 49 | { 50 | arg: struct { 51 | token string 52 | channel string 53 | }{ 54 | token: "", 55 | channel: "channel", 56 | }, 57 | success: false, 58 | }, 59 | { 60 | arg: struct { 61 | token string 62 | channel string 63 | }{ 64 | token: "token", 65 | channel: "", 66 | }, 67 | success: false, 68 | }, 69 | } 70 | 71 | for _, test := range tests { 72 | _, err := NewNotifier(test.arg.token, test.arg.channel) 73 | 74 | if (err == nil) != test.success { 75 | if test.success { 76 | t.Fatalf("Unexpected failed to initialize notifier: %s", err.Error()) 77 | } else { 78 | t.Fatalf("Unexpected successed to initialize notifier") 79 | } 80 | } 81 | } 82 | } 83 | 84 | func TestString(t *testing.T) { 85 | if String() != notifierName { 86 | t.Fatal("Unmatch return value of String() with notifierName") 87 | } 88 | } 89 | 90 | func TestAlert(t *testing.T) { 91 | type TestArg struct { 92 | backend *Slack 93 | expiration time.Time 94 | ingress *source.Ingress 95 | tls *source.IngressTLS 96 | opt notifier.Option 97 | } 98 | 99 | type TestCase struct { 100 | args TestArg 101 | success bool 102 | } 103 | 104 | tests := []TestCase{ 105 | { 106 | args: TestArg{ 107 | makeTestSlack(t, dummyToken, stubClientChannelName), 108 | time.Now(), 109 | makeTestIngress(t), 110 | makeTestIngressTLS(t), 111 | notifier.Option{AlertLevel: notifier.AlertLevelWarning}, 112 | }, 113 | success: true, 114 | }, 115 | { 116 | args: TestArg{ 117 | makeTestSlack(t, dummyToken, "Dummy"+stubClientChannelName), 118 | time.Now(), 119 | makeTestIngress(t), 120 | makeTestIngressTLS(t), 121 | notifier.Option{AlertLevel: notifier.AlertLevelCritical}, 122 | }, 123 | success: false, 124 | }, 125 | } 126 | 127 | for _, test := range tests { 128 | err := test.args.backend.Alert(test.args.expiration, test.args.ingress, test.args.tls, test.args.opt) 129 | 130 | if (err == nil) && test.success == false { 131 | t.Fatal("Unexpected result: Alert should be fail") 132 | } 133 | 134 | if (err != nil) && test.success == true { 135 | t.Fatalf("Unexpected result: %s", err.Error()) 136 | } 137 | } 138 | } 139 | 140 | func TestPostWithRateLimiter(t *testing.T) { 141 | s := makeTestSlack(t, dummyToken, stubClientChannelName) 142 | 143 | err := s.postWithRateLimiter(stubClientChannelName, "", libSlack.PostMessageParameters{}) 144 | if err != nil { 145 | t.Fatal("Raise error when testing postWithRateLimiter") 146 | } 147 | 148 | before := time.Now() 149 | 150 | err = s.postWithRateLimiter(stubClientChannelName, "", libSlack.PostMessageParameters{}) 151 | if err != nil { 152 | t.Fatal("Raise error when testing postWithRateLimiter") 153 | } 154 | 155 | after := time.Now() 156 | 157 | if after.Sub(before).Seconds() <= float64(sendPerSecond) { 158 | t.Fatalf("Rate limit is not being observed in %d per second. Actual: %f", sendPerSecond, after.Sub(before).Seconds()) 159 | } 160 | } 161 | 162 | func makeTestSlack(t *testing.T, token string, channel string) *Slack { 163 | t.Helper() 164 | return &Slack{ 165 | APIClient: &fakeClient{Token: token}, // Replace client to fake client 166 | ChannelName: channel, 167 | RateLimiter: ratelimit.New(sendPerSecond), 168 | } 169 | } 170 | 171 | func makeTestIngress(t *testing.T) *source.Ingress { 172 | t.Helper() 173 | return &source.Ingress{ 174 | ClusterName: "DummyClusterName", 175 | Namespace: "DummyNamespace", 176 | Name: "DummyName", 177 | TLS: []*source.IngressTLS{}, 178 | } 179 | } 180 | 181 | func makeTestIngressTLS(t *testing.T) *source.IngressTLS { 182 | t.Helper() 183 | return &source.IngressTLS{ 184 | Endpoints: []*source.TLSEndpoint{ 185 | source.NewTLSEndpoint("host01.example.com", ""), 186 | source.NewTLSEndpoint("host02.example.com", ""), 187 | }, 188 | SecretName: "DummySecretName", 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /source/ingress.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | // Ingress expresses information about existing Ingress. 4 | // Controller requires some fileds of original Ingress struct. 5 | // So, this definition masks unnecessary fields of https://godoc.org/k8s.io/api/extensions/v1beta1#Ingress 6 | type Ingress struct { 7 | ClusterName string 8 | Namespace string 9 | Name string 10 | TLS []*IngressTLS 11 | } 12 | -------------------------------------------------------------------------------- /source/ingress_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | -------------------------------------------------------------------------------- /source/ingress_tls.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | // IngressTLS expresses information about existing IngressTLS. 4 | // Controller requires some fileds of original Ingress struct. 5 | // So, this definition masks unnecessary fields of https://godoc.org/k8s.io/api/extensions/v1beta1#IngressTLS 6 | type IngressTLS struct { 7 | Endpoints []*TLSEndpoint 8 | SecretName string 9 | } 10 | -------------------------------------------------------------------------------- /source/ingress_tls_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | -------------------------------------------------------------------------------- /source/source.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "context" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/client-go/kubernetes" 8 | ) 9 | 10 | // Source struct defines abstruct client for Kubernetes API. 11 | // Source uses ClientSet to call API endpoint of Kubernetes. 12 | type Source struct { 13 | ClientSet kubernetes.Interface 14 | } 15 | 16 | // NewSource creates Source instance that defined Ingresses function. 17 | func NewSource(clientSet kubernetes.Interface) *Source { 18 | return &Source{ 19 | ClientSet: clientSet, 20 | } 21 | } 22 | 23 | // Ingresses returns list of Ingress that masked unnecessary fields 24 | // Ingress struct is defined by ingress.go 25 | func (s *Source) Ingresses() ([]*Ingress, error) { 26 | ingressList, err := s.ClientSet.NetworkingV1().Ingresses("").List(context.TODO(), metav1.ListOptions{}) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | ingresses := make([]*Ingress, len(ingressList.Items)) 32 | for i, item := range ingressList.Items { 33 | 34 | ingressTLSs := make([]*IngressTLS, len(item.Spec.TLS)) 35 | for j, tls := range item.Spec.TLS { 36 | 37 | endpoints := make([]*TLSEndpoint, len(tls.Hosts)) 38 | for k, host := range tls.Hosts { 39 | // TODO: Support port numbers other than default 40 | endpoints[k] = NewTLSEndpoint(host, "") 41 | } 42 | 43 | ingressTLSs[j] = &IngressTLS{ 44 | Endpoints: endpoints, 45 | SecretName: tls.SecretName, 46 | } 47 | } 48 | 49 | ingresses[i] = &Ingress{ 50 | ClusterName: item.ObjectMeta.ClusterName, 51 | Namespace: item.ObjectMeta.Namespace, 52 | Name: item.ObjectMeta.Name, 53 | TLS: ingressTLSs, 54 | } 55 | } 56 | 57 | return ingresses, nil 58 | } 59 | -------------------------------------------------------------------------------- /source/source_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "testing" 5 | 6 | v1 "k8s.io/api/networking/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/kubernetes/fake" 9 | ) 10 | 11 | func TestIngresses(t *testing.T) { 12 | ingressList := v1.IngressList{ 13 | Items: []v1.Ingress{ 14 | v1.Ingress{ 15 | ObjectMeta: metav1.ObjectMeta{ 16 | Name: "ingress1", 17 | Namespace: "namespace1", 18 | ClusterName: "clusterName", 19 | Labels: map[string]string{ 20 | "protocol": "tls", 21 | }, 22 | }, 23 | Spec: v1.IngressSpec{ 24 | TLS: []v1.IngressTLS{ 25 | { 26 | Hosts: []string{"1.example.com"}, 27 | SecretName: "ingressSecret1", 28 | }, 29 | }, 30 | }, 31 | }, 32 | v1.Ingress{ 33 | ObjectMeta: metav1.ObjectMeta{ 34 | Name: "ingress2", 35 | Namespace: "namespace2", 36 | ClusterName: "clusterName", 37 | Labels: map[string]string{ 38 | "protocol": "http", 39 | }, 40 | }, 41 | Spec: v1.IngressSpec{ 42 | TLS: []v1.IngressTLS{ 43 | { 44 | Hosts: []string{"2.example.com"}, 45 | SecretName: "ingressSecret2", 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | } 52 | 53 | clientSet := fake.NewSimpleClientset(&ingressList) 54 | source := NewSource(clientSet) 55 | actualIngresses, _ := source.Ingresses() 56 | 57 | expectedNum := len(ingressList.Items) 58 | if len(actualIngresses) != expectedNum { 59 | t.Fatalf("Unexpected number of Ingresses: %d", len(actualIngresses)) 60 | } 61 | 62 | for i, ingress := range actualIngresses { 63 | if ingress.Name != ingressList.Items[i].ObjectMeta.Name { 64 | t.Fatalf("Unmatch expected Name: %s", ingress.Name) 65 | } 66 | if ingress.Namespace != ingressList.Items[i].ObjectMeta.Namespace { 67 | t.Fatalf("Unmatch expected Namespace: %s", ingress.Namespace) 68 | } 69 | if ingress.ClusterName != ingressList.Items[i].ObjectMeta.ClusterName { 70 | t.Fatalf("Unmatch expected ClusterName: %s", ingress.ClusterName) 71 | } 72 | 73 | for j, tls := range ingress.TLS { 74 | if len(tls.Endpoints) != len(ingressList.Items[i].Spec.TLS[j].Hosts) { 75 | t.Fatalf("Unexpected number of TLS Hosts: %d", len(tls.Endpoints)) 76 | } 77 | if tls.SecretName != ingressList.Items[i].Spec.TLS[j].SecretName { 78 | t.Fatalf("Unmatch expected TLS SecretName: %s", tls.SecretName) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /source/tls_endpoint.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | // Allow certificate that signed by unknown authority. 11 | // Controller only concerns expiration of certificate. 12 | defaultTLSConfig = tls.Config{InsecureSkipVerify: true} 13 | 14 | // DefaultPortNumber exposes default port number to testing 15 | // TODO: Support port numbers other than :443 16 | DefaultPortNumber = "443" 17 | ) 18 | 19 | // TLSEndpoint expressses https endpoint that using TLS. 20 | type TLSEndpoint struct { 21 | Hostname string 22 | Port string 23 | } 24 | 25 | // NewTLSEndpoint creates new TLSEndpoint instance. 26 | // If port number is empty, set DefaultPortNumber instead. 27 | func NewTLSEndpoint(host string, port string) *TLSEndpoint { 28 | if port == "" { 29 | port = DefaultPortNumber 30 | } 31 | 32 | return &TLSEndpoint{ 33 | Hostname: host, 34 | Port: port, 35 | } 36 | } 37 | 38 | // GetCertificates tries to get certificates from endpoint using tls.Dial 39 | func (e *TLSEndpoint) GetCertificates() ([]*x509.Certificate, error) { 40 | 41 | // We cannot connect to Hostnames with wildcards, so replacing with cert-test. 42 | hostName := strings.Replace(e.Hostname, "*", "cert-test", -1) 43 | conn, err := tls.Dial("tcp", hostName+":"+e.Port, &defaultTLSConfig) 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer conn.Close() 48 | 49 | return conn.ConnectionState().PeerCertificates, nil 50 | } 51 | -------------------------------------------------------------------------------- /source/tls_endpoint_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestGetCertificates(t *testing.T) { 11 | testWithTLSServer(func(server *httptest.Server) { 12 | u, _ := url.Parse(server.URL) 13 | 14 | // When using available endpoint 15 | availableEndpoint := NewTLSEndpoint(u.Hostname(), u.Port()) 16 | certs, err := availableEndpoint.GetCertificates() 17 | if err != nil || len(certs) == 0 { 18 | t.Fatalf("Cannot get certificate when using available endpoint %s", u.Hostname()+":"+u.Port()) 19 | } 20 | 21 | // When using unavailable endpoint 22 | unavailableEndpoint := NewTLSEndpoint("dummy.localhost.local", "443") 23 | certs, err = unavailableEndpoint.GetCertificates() 24 | if err == nil || len(certs) != 0 { 25 | t.Fatalf("Unexpected result when using unavailable endpoint %s", u.Hostname()+":"+u.Port()) 26 | } 27 | }) 28 | } 29 | 30 | func testWithTLSServer(f func(server *httptest.Server)) { 31 | server := httptest.NewTLSServer(http.NewServeMux()) 32 | defer server.Close() 33 | f(server) 34 | } 35 | 36 | func TestNewTLSEndpoint(t *testing.T) { 37 | type testArg struct { 38 | Hostname string 39 | Port string 40 | } 41 | 42 | tests := []struct { 43 | args testArg 44 | expectedPort string 45 | }{ 46 | { 47 | args: testArg{Hostname: "example.com", Port: "5512"}, 48 | expectedPort: "5512", 49 | }, 50 | { 51 | args: testArg{Hostname: "*.example.com", Port: ""}, 52 | expectedPort: DefaultPortNumber, 53 | }, 54 | } 55 | 56 | for _, test := range tests { 57 | e := NewTLSEndpoint(test.args.Hostname, test.args.Port) 58 | if test.expectedPort != e.Port { 59 | t.Fatalf("Unexpected port number: %s", e.Port) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /synthetics/datadog/datadog.go: -------------------------------------------------------------------------------- 1 | package synthetics 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/zorkian/go-datadog-api" 7 | "go.uber.org/zap" 8 | "log" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // TestManager synchronize synthetics tests in Datadog with existing Kubernetes Ingress Endpoints 14 | type TestManager struct { 15 | Client Client 16 | Logger *zap.Logger 17 | AlertMessage string 18 | CheckInterval int 19 | DefaultTag string 20 | DefaultLocations []string 21 | Tags []string 22 | AdditionalEndpoints []string 23 | Enabled bool 24 | } 25 | 26 | // Client is an interface that clients implement to manage synthetic tests in Datadog. 27 | type Client interface { 28 | CreateSyntheticsTest(syntheticsTest *datadog.SyntheticsTest) (*datadog.SyntheticsTest, error) 29 | GetSyntheticsTests() ([]datadog.SyntheticsTest, error) 30 | DeleteSyntheticsTests(publicIds []string) error 31 | } 32 | 33 | type SyntheticEndpoint struct { 34 | Hostname string 35 | Port int 36 | } 37 | 38 | type SyntheticEndpoints map[string]SyntheticEndpoint 39 | 40 | func (s SyntheticEndpoint) GetNormalizedName() string { 41 | return fmt.Sprintf("%s-%d", s.Hostname, s.Port) 42 | } 43 | 44 | func (s SyntheticEndpoint) FromHostPortStr(hostname string, port string) (SyntheticEndpoint, error) { 45 | endpoint := fmt.Sprintf("%s:%s", hostname, port) 46 | return s.FromString(endpoint) 47 | } 48 | 49 | func (s SyntheticEndpoint) FromString(input string) (SyntheticEndpoint, error) { 50 | host := input 51 | portStr := "443" 52 | 53 | if strings.Contains(input, ":") { 54 | split := strings.Split(input, ":") 55 | 56 | if len(split) != 2 { 57 | return s, errors.New("Invalid additional endpoint " + input) 58 | } 59 | 60 | host = split[0] 61 | portStr = split[1] 62 | 63 | if len(host) == 0 { 64 | return s, errors.New("missing hostname") 65 | } 66 | 67 | if len(portStr) == 0 { 68 | return s, errors.New("missing port") 69 | } 70 | 71 | if portStr == "0" { 72 | return s, errors.New("invalid port") 73 | } 74 | } 75 | 76 | port, err := strconv.Atoi(portStr) 77 | if err != nil { 78 | return s, errors.New("The port number is not a valid numeral: " + portStr) 79 | } 80 | 81 | s.Hostname = host 82 | s.Port = port 83 | 84 | return s, nil 85 | } 86 | 87 | func (se SyntheticEndpoints) Add(s SyntheticEndpoint) { 88 | se[s.GetNormalizedName()] = s 89 | } 90 | 91 | // NewTestManager creates a new datadog.Client 92 | func NewTestManager(apiKey string, appKey string) (*TestManager, error) { 93 | if apiKey == "" { 94 | return &TestManager{}, errors.New("datadog api key is required") 95 | } 96 | if appKey == "" { 97 | return &TestManager{}, errors.New("datadog application key is required") 98 | } 99 | return &TestManager{ 100 | Client: datadog.NewClient(apiKey, appKey), 101 | }, nil 102 | } 103 | 104 | // Contains return whether a slice contains a specific value 105 | func Contains(slice []string, val string) bool { 106 | for _, n := range slice { 107 | if val == n { 108 | return true 109 | } 110 | } 111 | return false 112 | } 113 | 114 | // getManagedSyntheticsTests returns all synthetics tests matching the default tag 115 | func (tm *TestManager) getManagedSyntheticsTests() ([]datadog.SyntheticsTest, error) { 116 | // Return error if default tag is not set 117 | if tm.DefaultTag == "" { 118 | err := fmt.Errorf("No default tag is set for synthetics tests, aborting creation process") 119 | return nil, err 120 | } 121 | var managedTests []datadog.SyntheticsTest 122 | // Get all existing synthetic tests 123 | tests, err := tm.Client.GetSyntheticsTests() 124 | if err != nil { 125 | log.Printf("Failed to get synthetics tests from Datadog: %s\n", err.Error()) 126 | return nil, err 127 | } 128 | for _, test := range tests { 129 | // Only deal with tests having auto-generated tag 130 | if Contains(test.Tags, tm.DefaultTag) { 131 | if _, exists := test.GetNameOk(); exists { 132 | managedTests = append(managedTests, test) 133 | } 134 | } 135 | } 136 | 137 | return managedTests, nil 138 | } 139 | 140 | // createManagedSyntheticsTest configures and create a new synthetics test in Datadog 141 | func (tm *TestManager) createManagedSyntheticsTest(name string, endpoint SyntheticEndpoint) (*datadog.SyntheticsTest, error) { 142 | newOptions := &datadog.SyntheticsOptions{} 143 | newOptions.SetAcceptSelfSigned(false) 144 | newOptions.SetTickEvery(tm.CheckInterval) 145 | 146 | expiryAssertion := &datadog.SyntheticsAssertion{} 147 | expiryAssertion.SetType("certificate") 148 | expiryAssertion.SetOperator("isInMoreThan") 149 | expiryAssertion.Target = 12 150 | 151 | newRequest := &datadog.SyntheticsRequest{} 152 | newRequest.SetHost(endpoint.Hostname) 153 | newRequest.SetPort(endpoint.Port) 154 | 155 | newConfig := &datadog.SyntheticsConfig{} 156 | newConfig.Assertions = []datadog.SyntheticsAssertion{*expiryAssertion} 157 | newConfig.SetRequest(*newRequest) 158 | 159 | tags := tm.Tags 160 | tags = append(tags, tm.DefaultTag) 161 | 162 | newTest := &datadog.SyntheticsTest{Locations: tm.DefaultLocations, Tags: tags} 163 | newTest.SetName(name) 164 | newTest.SetType("api") 165 | newTest.SetSubtype("ssl") 166 | newTest.SetConfig(*newConfig) 167 | 168 | newTest.SetMessage(tm.AlertMessage) 169 | newTest.SetOptions(*newOptions) 170 | 171 | test, err := tm.Client.CreateSyntheticsTest(newTest) 172 | if err != nil { 173 | return nil, err 174 | } 175 | return test, nil 176 | } 177 | 178 | // CreateManagedSyntheticsTests creates synthetics test according to the endpointList provided 179 | func (tm *TestManager) CreateManagedSyntheticsTests(endpoints SyntheticEndpoints) error { 180 | // Get all existing synthetic tests 181 | tests, err := tm.getManagedSyntheticsTests() 182 | if err != nil { 183 | log.Printf("Failed to get synthetics tests: %s\n", err.Error()) 184 | return err 185 | } 186 | for name, endpoint := range endpoints { 187 | var matched bool 188 | 189 | // Normalize endpoint names from SYNTHETIC_ADDITIONAL_ENDPOINTS as they might have a defined port 190 | for _, test := range tests { 191 | if name == test.GetName() { 192 | matched = true 193 | } 194 | } 195 | if matched { 196 | log.Printf("Test is already existing for %s:%d and Ingress exists", endpoint.Hostname, endpoint.Port) 197 | } else { 198 | log.Printf("Creating new test for Ingress endpoint %s:%d", endpoint.Hostname, endpoint.Port) 199 | _, err := tm.createManagedSyntheticsTest(name, endpoint) 200 | if err != nil { 201 | log.Printf("Couldn't create the synthetic test for Ingress endpoint %s:%d: %s\n", endpoint.Hostname, endpoint.Port, err.Error()) 202 | } 203 | } 204 | } 205 | return nil 206 | } 207 | 208 | // DeleteManagedSyntheticsTests removes managed synthetics test not matching the endpointList provided 209 | func (tm *TestManager) DeleteManagedSyntheticsTests(endpoints SyntheticEndpoints) error { 210 | // Get all existing synthetic tests 211 | tests, err := tm.getManagedSyntheticsTests() 212 | if err != nil { 213 | log.Printf("Failed to get synthetics tests: %s\n", err.Error()) 214 | return err 215 | } 216 | // Slice containing all tests publicIds to delete 217 | var toDelete []string 218 | for _, test := range tests { 219 | if _, ok := endpoints[test.GetName()]; !ok { 220 | log.Printf("warn: Managed test %s, with hostname %s, doesn't have any matching ingress, adding to delete list", test.GetPublicId(), test.GetName()) 221 | toDelete = append(toDelete, test.GetPublicId()) 222 | } 223 | } 224 | // Delete only when there are candidates to deletion 225 | if toDelete != nil && len(toDelete) >= 1 { 226 | log.Printf("Deleting %d managed tests", len(toDelete)) 227 | err := tm.Client.DeleteSyntheticsTests(toDelete) 228 | if err != nil { 229 | log.Printf("Failed to delete managed tests: %s\n", err.Error()) 230 | return err 231 | } 232 | } else { 233 | log.Printf("No test candidate for deletion") 234 | } 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /synthetics/datadog/datadog_test.go: -------------------------------------------------------------------------------- 1 | package synthetics 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "os" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/zorkian/go-datadog-api" 12 | ) 13 | 14 | type fakeClient struct { 15 | t *testing.T 16 | 17 | validateCreateSyntheticsTestFunc func(t *testing.T, syntheticsTest *datadog.SyntheticsTest) (*datadog.SyntheticsTest, error) 18 | validateGetSyntheticsTestsFunc func(t *testing.T) []datadog.SyntheticsTest 19 | validateDeleteSyntheticsTestsFunc func(t *testing.T, publicIds []string) error 20 | } 21 | 22 | func (f *fakeClient) CreateSyntheticsTest(syntheticsTest *datadog.SyntheticsTest) (*datadog.SyntheticsTest, error) { 23 | test, _ := f.validateCreateSyntheticsTestFunc(f.t, syntheticsTest) 24 | return test, nil 25 | } 26 | 27 | func (f *fakeClient) GetSyntheticsTests() ([]datadog.SyntheticsTest, error) { 28 | tests := f.validateGetSyntheticsTestsFunc(f.t) 29 | return tests, nil 30 | } 31 | 32 | func (f *fakeClient) DeleteSyntheticsTests(publicIds []string) error { 33 | error := f.validateDeleteSyntheticsTestsFunc(f.t, publicIds) 34 | return error 35 | } 36 | 37 | func TestCreateSyntheticsTest(t *testing.T) { 38 | client := &fakeClient{ 39 | t: t, 40 | validateCreateSyntheticsTestFunc: func(t *testing.T, syntheticsTest *datadog.SyntheticsTest) (*datadog.SyntheticsTest, error) { 41 | if len(syntheticsTest.GetConfig().Assertions) != 1 { 42 | t.Fatalf("got %v, want %v", len(syntheticsTest.GetConfig().Assertions), 1) 43 | } 44 | if got, want := *syntheticsTest.GetOptions().AcceptSelfSigned, false; got != want { 45 | t.Fatalf("got %v, want %v", got, want) 46 | } 47 | if got, want := *syntheticsTest.GetOptions().TickEvery, 60; got != want { 48 | t.Fatalf("got %v, want %v", got, want) 49 | } 50 | if got, want := syntheticsTest.GetConfig().Assertions[0].GetType(), "certificate"; got != want { 51 | t.Fatalf("got %v, want %v", got, want) 52 | } 53 | if got, want := syntheticsTest.GetConfig().Assertions[0].GetOperator(), "isInMoreThan"; got != want { 54 | t.Fatalf("got %v, want %v", got, want) 55 | } 56 | if got, want := syntheticsTest.GetConfig().Assertions[0].Target, 12; got != want { 57 | t.Fatalf("got %v, want %v", got, want) 58 | } 59 | if got, want := syntheticsTest.GetConfig().Request.GetPort(), 443; got != want { 60 | t.Fatalf("got %v, want %v", got, want) 61 | } 62 | if got, want := syntheticsTest.GetConfig().Request.GetHost(), "example.com"; got != want { 63 | t.Fatalf("got %v, want %v", got, want) 64 | } 65 | if got, want := syntheticsTest.GetType(), "api"; got != want { 66 | t.Fatalf("got %v, want %v", got, want) 67 | } 68 | if got, want := syntheticsTest.GetSubtype(), "ssl"; got != want { 69 | t.Fatalf("got %v, want %v", got, want) 70 | } 71 | if len(syntheticsTest.Locations) != 1 { 72 | t.Fatalf("got %v, want %v", len(syntheticsTest.Locations), 1) 73 | } 74 | if got, want := syntheticsTest.Locations[0], "aws:ap-northeast-1"; got != want { 75 | t.Fatalf("got %v, want %v", got, want) 76 | } 77 | if len(syntheticsTest.Tags) != 1 { 78 | t.Fatalf("got %v, want %v", len(syntheticsTest.Tags), 1) 79 | } 80 | if got, want := syntheticsTest.Tags[0], "managed-by-cert-exp-mon"; got != want { 81 | t.Fatalf("got %v, want %v", got, want) 82 | } 83 | return syntheticsTest, nil 84 | }, 85 | } 86 | 87 | apiKey := "api_key" 88 | appKey := "app_key" 89 | tm, err := NewTestManager(apiKey, appKey) 90 | if err != nil { 91 | t.Fatalf("want nil, got %s", err) 92 | } 93 | tm.CheckInterval = 60 94 | tm.AlertMessage = "" 95 | tm.Client = client 96 | tm.DefaultTag = "managed-by-cert-exp-mon" 97 | tm.DefaultLocations = []string{"aws:ap-northeast-1"} 98 | name := "example.com-443" 99 | endpoint := SyntheticEndpoint{ 100 | Hostname: "example.com", 101 | Port: 443, 102 | } 103 | tm.createManagedSyntheticsTest(name, endpoint) 104 | } 105 | 106 | func TestGetSyntheticsTests(t *testing.T) { 107 | client := &fakeClient{ 108 | t: t, 109 | validateGetSyntheticsTestsFunc: func(t *testing.T) []datadog.SyntheticsTest { 110 | test := new(datadog.SyntheticsTest) 111 | test2 := new(datadog.SyntheticsTest) 112 | tests := &[]datadog.SyntheticsTest{*test, *test2} 113 | return *tests 114 | }, 115 | } 116 | 117 | apiKey := "api_key" 118 | appKey := "app_key" 119 | tm, err := NewTestManager(apiKey, appKey) 120 | if err != nil { 121 | t.Fatalf("want nil, got %s", err) 122 | } 123 | tm.Client = client 124 | if got, _ := tm.Client.GetSyntheticsTests(); got == nil { 125 | t.Fatal("want []datadog.SyntheticsTest, got nil") 126 | } 127 | 128 | } 129 | 130 | func TestNewTestManager(t *testing.T) { 131 | apiKey := "api_key" 132 | appKey := "app_key" 133 | if got, _ := NewTestManager(apiKey, appKey); got == nil { 134 | t.Fatal("want *NewTestManager, got nil") 135 | } 136 | } 137 | 138 | func TestCreateManagedSyntheticsTests(t *testing.T) { 139 | client := &fakeClient{ 140 | t: t, 141 | validateGetSyntheticsTestsFunc: func(t *testing.T) []datadog.SyntheticsTest { 142 | test := new(datadog.SyntheticsTest) 143 | test.SetName("example.com-443") 144 | test.Tags = append(test.Tags, "managed-by-cert-expiry-mon") 145 | test2 := new(datadog.SyntheticsTest) 146 | test2.SetName("example2.com-443") 147 | tests := &[]datadog.SyntheticsTest{*test, *test2} 148 | return *tests 149 | }, 150 | validateCreateSyntheticsTestFunc: func(t *testing.T, syntheticsTest *datadog.SyntheticsTest) (*datadog.SyntheticsTest, error) { 151 | return syntheticsTest, nil 152 | }, 153 | } 154 | 155 | apiKey := "api_key" 156 | appKey := "app_key" 157 | tm, err := NewTestManager(apiKey, appKey) 158 | if err != nil { 159 | t.Fatalf("want nil, got %s", err) 160 | } 161 | tm.Client = client 162 | tm.DefaultTag = "managed-by-cert-expiry-mon" 163 | 164 | if got, _ := tm.Client.GetSyntheticsTests(); got == nil { 165 | t.Fatal("want []datadog.SyntheticsTest, got nil") 166 | } 167 | 168 | if got := captureOutput(func() { 169 | endpoints := SyntheticEndpoints{ 170 | "example.com-443": SyntheticEndpoint{ 171 | Hostname: "example.com", 172 | Port: 443, 173 | }, 174 | } 175 | tm.CreateManagedSyntheticsTests(endpoints) 176 | }); strings.Contains(got, "Test is already existing for") == false { 177 | t.Fatalf("want `Test is already existing for example.com-443 and Ingress exists`, got %s", got) 178 | } 179 | if got := captureOutput(func() { 180 | endpoints := SyntheticEndpoints{ 181 | "nonexistinguri.com-443": SyntheticEndpoint{ 182 | Hostname: "nonexistinguri.com", 183 | Port: 443, 184 | }, 185 | } 186 | tm.CreateManagedSyntheticsTests(endpoints) 187 | }); strings.Contains(got, "Creating new test for Ingress endpoint nonexistinguri.com") == false { 188 | t.Fatalf("want test, got %s", got) 189 | } 190 | 191 | } 192 | 193 | func TestDeleteManagedSyntheticsTests(t *testing.T) { 194 | client := &fakeClient{ 195 | t: t, 196 | validateGetSyntheticsTestsFunc: func(t *testing.T) []datadog.SyntheticsTest { 197 | test := new(datadog.SyntheticsTest) 198 | test.SetName("example.com-443") 199 | test.SetPublicId("aaa-aaa-aaa") 200 | test.Tags = append(test.Tags, "managed-by-cert-expiry-mon") 201 | 202 | test2 := new(datadog.SyntheticsTest) 203 | test.SetPublicId("bbb-bbb-bbb") 204 | test2.SetName("example2.com-443") 205 | 206 | test3 := new(datadog.SyntheticsTest) 207 | test3.SetPublicId("ccc-ccc-ccc") 208 | test3.SetName("example3.com-443") 209 | test3.Tags = append(test.Tags, "managed-by-cert-expiry-mon") 210 | 211 | tests := &[]datadog.SyntheticsTest{*test, *test2, *test3} 212 | return *tests 213 | }, 214 | validateDeleteSyntheticsTestsFunc: func(t *testing.T, publicIds []string) error { 215 | return nil 216 | }, 217 | } 218 | 219 | apiKey := "api_key" 220 | appKey := "app_key" 221 | tm, _ := NewTestManager(apiKey, appKey) 222 | tm.Client = client 223 | tm.DefaultTag = "managed-by-cert-expiry-mon" 224 | 225 | // Case 1: Only example3.com should be deleted, example.com is in the Ingress endpoint list and example2 doesn't have the managed tag 226 | if got := captureOutput(func() { 227 | endpoints := SyntheticEndpoints{ 228 | "example.com-443": SyntheticEndpoint{ 229 | Hostname: "example.com", 230 | Port: 443, 231 | }, 232 | } 233 | tm.DeleteManagedSyntheticsTests(endpoints) 234 | }); strings.Contains(got, "Managed test ccc-ccc-ccc, with hostname example3.com-443, doesn't have any matching ingress, adding to delete list") == false { 235 | t.Fatalf("want `Managed test ccc-ccc-ccc, with hostname example3.com-443, doesn't have any matching ingress, adding to delete list`, got %s", got) 236 | } 237 | // Case 2: example.com and example3.com should be deleted, example2.com doesn't have the tag 238 | if got := captureOutput(func() { 239 | endpoints := SyntheticEndpoints{} 240 | tm.DeleteManagedSyntheticsTests(endpoints) 241 | }); strings.Contains(got, "Deleting 2 managed tests") == false { 242 | t.Fatalf("want `Deleting 2 managed tests`, got %s", got) 243 | } 244 | // Case 3: Nothing should be deleted, expect no output 245 | if got := captureOutput(func() { 246 | endpoints := SyntheticEndpoints{ 247 | "example.com-443": SyntheticEndpoint{ 248 | Hostname: "example.com", 249 | Port: 443, 250 | }, 251 | "example3.com-443": SyntheticEndpoint{ 252 | Hostname: "example3.com", 253 | Port: 443, 254 | }, 255 | } 256 | tm.DeleteManagedSyntheticsTests(endpoints) 257 | }); strings.Contains(got, "No test candidate for deletion") == false { 258 | t.Fatalf("want `No test candidate for deletion`, got %s", got) 259 | } 260 | // Case 4: example2.com should not be deleted as it doesn't have the managed tag, expect no output 261 | if got := captureOutput(func() { 262 | endpoints := SyntheticEndpoints{ 263 | "example2.com-443": SyntheticEndpoint{ 264 | Hostname: "example2.com", 265 | Port: 443, 266 | }, 267 | } 268 | 269 | tm.DeleteManagedSyntheticsTests(endpoints) 270 | }); strings.Contains(got, "re") == false { 271 | t.Fatalf("want `Deleting 2 managed tests`, got %s", got) 272 | } 273 | } 274 | 275 | func captureOutput(f func()) string { 276 | var buf bytes.Buffer 277 | log.SetOutput(&buf) 278 | f() 279 | log.SetOutput(os.Stderr) 280 | return buf.String() 281 | } 282 | 283 | func TestSyntheticEndpoint_GetNormalizedName(t *testing.T) { 284 | type fields struct { 285 | Hostname string 286 | Port int 287 | } 288 | tests := []struct { 289 | name string 290 | fields fields 291 | want string 292 | }{ 293 | { 294 | name: "SimpleCase", 295 | fields: fields{ 296 | Hostname: "test.com", 297 | Port: 443, 298 | }, 299 | want: "test.com-443", 300 | }, 301 | { 302 | name: "SimpleCase2", 303 | fields: fields{ 304 | Hostname: "test2.com", 305 | Port: 10000, 306 | }, 307 | want: "test2.com-10000", 308 | }, 309 | } 310 | for _, tt := range tests { 311 | t.Run(tt.name, func(t *testing.T) { 312 | s := SyntheticEndpoint{ 313 | Hostname: tt.fields.Hostname, 314 | Port: tt.fields.Port, 315 | } 316 | if got := s.GetNormalizedName(); got != tt.want { 317 | t.Errorf("GetNormalizedName() = %v, want %v", got, tt.want) 318 | } 319 | }) 320 | } 321 | } 322 | 323 | func TestSyntheticEndpoint_FromHostPortStr(t *testing.T) { 324 | type args struct { 325 | hostname string 326 | port string 327 | } 328 | tests := []struct { 329 | name string 330 | args args 331 | want SyntheticEndpoint 332 | wantErr bool 333 | }{ 334 | { 335 | name: "SimpleCase", 336 | args: args{ 337 | hostname: "example.com", 338 | port: "443", 339 | }, 340 | want: SyntheticEndpoint{ 341 | Hostname: "example.com", 342 | Port: 443, 343 | }, 344 | wantErr: false, 345 | }, 346 | { 347 | name: "SimpleCase2", 348 | args: args{ 349 | hostname: "example2.com", 350 | port: "1000", 351 | }, 352 | want: SyntheticEndpoint{ 353 | Hostname: "example2.com", 354 | Port: 1000, 355 | }, 356 | wantErr: false, 357 | }, 358 | { 359 | name: "MissingPort", 360 | args: args{ 361 | hostname: "example.com", 362 | port: "", 363 | }, 364 | want: SyntheticEndpoint{ 365 | Hostname: "", 366 | Port: 0, 367 | }, 368 | wantErr: true, 369 | }, 370 | } 371 | for _, tt := range tests { 372 | t.Run(tt.name, func(t *testing.T) { 373 | s := SyntheticEndpoint{ 374 | Hostname: "", 375 | Port: 0, 376 | } 377 | got, err := s.FromHostPortStr(tt.args.hostname, tt.args.port) 378 | if (err != nil) != tt.wantErr { 379 | t.Errorf("FromHostPortStr() error = %v, wantErr %v", err, tt.wantErr) 380 | return 381 | } 382 | if !reflect.DeepEqual(got, tt.want) { 383 | t.Errorf("FromHostPortStr() got = %v, want %v", got, tt.want) 384 | } 385 | }) 386 | } 387 | } 388 | 389 | func TestSyntheticEndpoint_FromString(t *testing.T) { 390 | type args struct { 391 | input string 392 | } 393 | tests := []struct { 394 | name string 395 | args args 396 | want SyntheticEndpoint 397 | wantErr bool 398 | }{ 399 | { 400 | name: "SimpleCase", 401 | args: args{ 402 | input: "example.com:443", 403 | }, 404 | want: SyntheticEndpoint{ 405 | Hostname: "example.com", 406 | Port: 443, 407 | }, 408 | wantErr: false, 409 | }, 410 | { 411 | name: "SimpleCase2", 412 | args: args{ 413 | input: "example2.com:1000", 414 | }, 415 | want: SyntheticEndpoint{ 416 | Hostname: "example2.com", 417 | Port: 1000, 418 | }, 419 | wantErr: false, 420 | }, 421 | { 422 | name: "MissingPort", 423 | args: args{ 424 | input: "example.com:", 425 | }, 426 | want: SyntheticEndpoint{ 427 | Hostname: "", 428 | Port: 0, 429 | }, 430 | wantErr: true, 431 | }, 432 | { 433 | name: "InvalidPort1", 434 | args: args{ 435 | input: "example.com:0", 436 | }, 437 | want: SyntheticEndpoint{ 438 | Hostname: "", 439 | Port: 0, 440 | }, 441 | wantErr: true, 442 | }, 443 | { 444 | name: "InvalidPort2", 445 | args: args{ 446 | input: "example.com:hello", 447 | }, 448 | want: SyntheticEndpoint{ 449 | Hostname: "", 450 | Port: 0, 451 | }, 452 | wantErr: true, 453 | }, 454 | { 455 | name: "MissingHostname", 456 | args: args{ 457 | input: ":443", 458 | }, 459 | want: SyntheticEndpoint{ 460 | Hostname: "", 461 | Port: 0, 462 | }, 463 | wantErr: true, 464 | }, 465 | } 466 | for _, tt := range tests { 467 | t.Run(tt.name, func(t *testing.T) { 468 | s := SyntheticEndpoint{ 469 | Hostname: "", 470 | Port: 0, 471 | } 472 | got, err := s.FromString(tt.args.input) 473 | if (err != nil) != tt.wantErr { 474 | t.Errorf("FromString() error = %v, wantErr %v", err, tt.wantErr) 475 | return 476 | } 477 | if !reflect.DeepEqual(got, tt.want) { 478 | t.Errorf("FromString() got = %v, want %v", got, tt.want) 479 | } 480 | }) 481 | } 482 | } 483 | 484 | func TestSyntheticEndpoints_Add(t *testing.T) { 485 | t.Run("AddAnEndpoint", func(t *testing.T) { 486 | se := SyntheticEndpoints{} 487 | 488 | se.Add(SyntheticEndpoint{ 489 | Hostname: "test.com", 490 | Port: 443, 491 | }) 492 | 493 | if _, ok := se["test.com-443"]; !ok { 494 | t.Errorf("Expected added endpoint to appear in the SyntheticEndpoints map") 495 | } 496 | }) 497 | } 498 | --------------------------------------------------------------------------------