├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .gitignore ├── .release ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config └── config.go ├── go.mod ├── go.sum ├── hack ├── deploy.yaml └── release.sh ├── helm └── kubediff │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── configmap.yaml │ ├── deployment.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── kubediff.png ├── main.go ├── pkg ├── event │ └── event.go ├── log │ └── log.go ├── notify │ ├── noop.go │ ├── notify.go │ ├── slack.go │ └── webhook.go └── watcher │ ├── client.go │ ├── handlers.go │ ├── informer.go │ ├── utils.go │ └── watcher.go └── test └── config.yaml /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ if .Versions -}} 2 | 3 | ## [Unreleased] 4 | 5 | {{ if .Unreleased.CommitGroups -}} 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title }} 8 | {{ range .Commits -}} 9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 10 | {{ end }} 11 | {{ end -}} 12 | {{ end -}} 13 | {{ end -}} 14 | 15 | {{ range .Versions }} 16 | 17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} 18 | {{ range .CommitGroups -}} 19 | ### {{ .Title }} 20 | {{ range .Commits -}} 21 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 22 | {{ end }} 23 | {{ end -}} 24 | 25 | {{- if .RevertCommits -}} 26 | ### Reverts 27 | {{ range .RevertCommits -}} 28 | - {{ .Revert.Header }} 29 | {{ end }} 30 | {{ end -}} 31 | 32 | {{- if .MergeCommits -}} 33 | ### Pull Requests 34 | {{ range .MergeCommits -}} 35 | - {{ .Header }} 36 | {{ end }} 37 | {{ end -}} 38 | 39 | {{- if .NoteGroups -}} 40 | {{ range .NoteGroups -}} 41 | ### {{ .Title }} 42 | {{ range .Notes }} 43 | {{ .Body }} 44 | {{ end }} 45 | {{ end -}} 46 | {{ end -}} 47 | {{ end -}} 48 | 49 | {{- if .Versions }} 50 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD 51 | {{ range .Versions -}} 52 | {{ if .Tag.Previous -}} 53 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 54 | {{ end -}} 55 | {{ end -}} 56 | {{ end -}} -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/arriqaaq/kubediff 6 | options: 7 | commits: 8 | # filters: 9 | # Type: 10 | # - feat 11 | # - fix 12 | # - perf 13 | # - refactor 14 | commit_groups: 15 | # title_maps: 16 | # feat: Features 17 | # fix: Bug Fixes 18 | # perf: Performance Improvements 19 | # refactor: Code Refactoring 20 | header: 21 | pattern: "^(\\w*)\\:\\s(.*)$" 22 | pattern_maps: 23 | - Type 24 | - Subject 25 | notes: 26 | keywords: 27 | - BREAKING CHANGE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.release: -------------------------------------------------------------------------------- 1 | release=v0.0.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arriqaaq/kubediff/a9d1958f81a27c3aaca4f0cd174f90c4b95ac7e9/CHANGELOG.md -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | RUN go mod download 8 | 9 | COPY main.go main.go 10 | COPY pkg/ pkg/ 11 | COPY config/ config/ 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o kubediff main.go 14 | 15 | 16 | 17 | FROM gcr.io/distroless/static:nonroot 18 | WORKDIR / 19 | COPY --from=builder /app/kubediff . 20 | USER nonroot:nonroot 21 | 22 | ENTRYPOINT ["/kubediff"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Farhan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_REPO=docker.io/arriqaaq/kubediff 2 | TAG=$(shell cut -d'=' -f2- .release) 3 | 4 | .DEFAULT_GOAL := build 5 | .PHONY: release git-tag check-git-status build image tag-image publish 6 | 7 | release: check-git-status image tag-image publish git-tag 8 | @echo "Successfully released version $(TAG)" 9 | 10 | git-tag: 11 | @echo "Creating a git tag" 12 | @git add .release helm/kubediff hack/deploy.yaml CHANGELOG.md 13 | @git commit -m "Release $(TAG)" ; 14 | @git tag ${TAG} ; 15 | @git push --tags; 16 | @echo 'Git tag pushed successfully' ; 17 | 18 | check-git-status: 19 | @echo "Checking git status" 20 | @if [ -n "$(shell git tag | grep $(TAG))" ] ; then echo 'ERROR: Tag already exists' && exit 1 ; fi 21 | 22 | build: 23 | GOOS_VAL=$(shell go env GOOS) GOARCH_VAL=$(shell go env GOARCH) go build -o $(shell go env GOPATH)/bin/kubediff 24 | @echo "Build complete" 25 | 26 | image: 27 | @echo "Building docker image" 28 | @docker build -t $(IMAGE_REPO) -f Dockerfile --no-cache . 29 | @echo "Docker image built" 30 | 31 | tag-image: 32 | @echo 'Tagging image' 33 | @docker tag $(IMAGE_REPO) $(IMAGE_REPO):$(TAG) 34 | 35 | publish: 36 | @echo "Pushing docker image to repository" 37 | @docker login 38 | @docker push $(IMAGE_REPO):$(TAG) 39 | @docker push $(IMAGE_REPO):latest 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 |

6 | 7 | # kubediff 8 | 9 | **kubediff** is a Kubernetes resource diff watcher, with the ability to send event notifications. 10 | 11 | [![asciicast](https://asciinema.org/a/6Hi2rnrJFjrfdG8SpNE7wy9m7.png)](https://asciinema.org/a/6Hi2rnrJFjrfdG8SpNE7wy9m7) 12 | 13 | # Usage 14 | ``` 15 | $ kubediff --config=/path/to/config 16 | 17 | kubediff: A resource diff watcher for Kubernetes 18 | 19 | kubediff is a Kubernetes resource diff watcher with the ability to configure event notifications 20 | to webhook/Slack. It watches the cluster for any resource change (including custom CRDs) and logs them. You can also run it in normal mode, and can export the logs to your preferred logging stack. 21 | 22 | Usage: 23 | kubediff --config=/path/to/config 24 | 25 | Flags: 26 | --config configuration folder for kubediff 27 | 28 | ``` 29 | 30 | # Install 31 | 32 | #### Using helm: 33 | 34 | When you have helm installed in your cluster, use the following setup: 35 | 36 | ```console 37 | helm repo add arriqaaq https://arriqaaq.github.io/charts 38 | helm repo update 39 | helm install --create-namespace --namespace kubediff kubediff arriqaaq/kubediff 40 | ``` 41 | 42 | You can also install this chart locally by cloning this repo: 43 | 44 | ```console 45 | helm install --create-namespace --namespace kubediff kubediff helm/kubediff 46 | ``` 47 | 48 | 49 | #### Using kubectl: 50 | 51 | In order to run kubediff in a kind cluster quickly, just run 52 | 53 | ```console 54 | $ kubectl apply -f hack/deploy.yaml 55 | ``` 56 | 57 | 58 | #### Configuration: 59 | You can also provide a custom config file: 60 | 61 | ```yaml 62 | resources: 63 | - kind: v1/pods # Name of the resource. Resource name must be in group/version/resource (G/V/R) format 64 | # resource name should be plural (e.g apps/v1/deployments, v1/pods) 65 | - kind: v1/services 66 | - kind: apps/v1/deployments 67 | - kind: apps/v1/statefulsets 68 | - kind: networking.k8s.io/v1beta1/ingresses 69 | - kind: v1/nodes 70 | - kind: v1/namespaces 71 | - kind: v1/persistentvolumes 72 | - kind: v1/persistentvolumeclaims 73 | - kind: v1/configmaps 74 | - kind: apps/v1/daemonsets 75 | - kind: batch/v1/jobs 76 | - kind: rbac.authorization.k8s.io/v1/roles 77 | - kind: rbac.authorization.k8s.io/v1/rolebindings 78 | - kind: rbac.authorization.k8s.io/v1/clusterrolebindings 79 | - kind: rbac.authorization.k8s.io/v1/clusterroles 80 | 81 | # watch multiple namespaces (or use **all** to watch all namespaces) 82 | namespaces: 83 | - all 84 | ``` 85 | 86 | 87 | #### Using Go: 88 | 89 | ```console 90 | # Download and install kubediff 91 | $ go get -u github.com/arriqaaq/kubediff 92 | 93 | # Add resources to be watched 94 | kubediff --config=./test/ 95 | 96 | ``` 97 | 98 | # Resources 99 | 100 | Read more on how it is implemented [here](https://aly.arriqaaq.com/kubernetes-informers/) 101 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | const ( 12 | WatchMode RunMode = "watch" 13 | DiffMode RunMode = "diff" 14 | ) 15 | 16 | type RunMode string 17 | 18 | type Config struct { 19 | Mode RunMode 20 | Resources []Resource 21 | Namespaces []string 22 | Notifier Notifier 23 | } 24 | 25 | func (c *Config) init() { 26 | if c.Mode == "" { 27 | c.Mode = WatchMode 28 | } 29 | 30 | if len(c.Namespaces) == 0 { 31 | c.Namespaces = append(c.Namespaces, "all") 32 | } 33 | } 34 | 35 | func (c *Config) validate() error { 36 | for _, ns := range c.Namespaces { 37 | if ns == "all" { 38 | if len(c.Namespaces) > 1 { 39 | return errors.New("cannot specify a namespace after selecting all") 40 | } 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | type Resource struct { 47 | Kind string 48 | } 49 | 50 | type Notifier struct { 51 | Slack Slack 52 | Webhook Webhook 53 | NoOp NoOp 54 | } 55 | 56 | // Slack contains slack configuration 57 | type Slack struct { 58 | Enabled bool 59 | Token string 60 | Channel string 61 | Title string 62 | } 63 | 64 | type Webhook struct { 65 | Enabled bool 66 | Url string 67 | } 68 | 69 | type NoOp struct { 70 | Enabled bool 71 | } 72 | 73 | // New returns new Config 74 | func New(filepath string) (*Config, error) { 75 | c := &Config{} 76 | config, err := os.Open(filepath) 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer config.Close() 81 | 82 | b, err := ioutil.ReadAll(config) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | if len(b) != 0 { 88 | yaml.Unmarshal(b, c) 89 | } 90 | 91 | c.init() 92 | err = c.validate() 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return c, nil 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arriqaaq/kubediff 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-test/deep v1.0.7 7 | github.com/sirupsen/logrus v1.8.1 8 | github.com/slack-go/slack v0.9.5 9 | gopkg.in/yaml.v2 v2.4.0 10 | k8s.io/apimachinery v0.22.2 11 | k8s.io/client-go v0.22.2 12 | ) 13 | -------------------------------------------------------------------------------- /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/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 13 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 14 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 15 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 16 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 17 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 18 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 19 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 20 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 21 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 22 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 23 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 24 | github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 25 | github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= 26 | github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= 27 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= 28 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 29 | github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 30 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= 31 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 32 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 33 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 34 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 35 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 36 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 37 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 38 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 39 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 40 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 41 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 42 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 43 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 45 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 47 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 48 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 49 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 50 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 51 | github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= 52 | github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 53 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 54 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 55 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 56 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 57 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 58 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 59 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 60 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 61 | github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= 62 | github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 63 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 64 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 65 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 66 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 67 | github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= 68 | github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= 69 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 70 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 71 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 72 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 73 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 74 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 75 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 76 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 77 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 78 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 79 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 80 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 81 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 82 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 83 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 84 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 85 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 86 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 87 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 88 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 89 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 90 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 91 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 92 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 93 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 94 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 95 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 96 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 97 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 98 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 99 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 100 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 101 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 102 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 103 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 104 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 106 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 107 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 108 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 109 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 110 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 111 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 112 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 113 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 114 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 115 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 116 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 117 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 118 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 119 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 120 | github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= 121 | github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= 122 | github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= 123 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 124 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 125 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 126 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 127 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 128 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 129 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 130 | github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 131 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 132 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 133 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 134 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 135 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 136 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 137 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 138 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 139 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 140 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 141 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 142 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 143 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 144 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 145 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 146 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 147 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 148 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 149 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 150 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 151 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 152 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 153 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 154 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 155 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 156 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 157 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 158 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 159 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 160 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 161 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 162 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 163 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 164 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 165 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 166 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 167 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 168 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 169 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 170 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 171 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 172 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 173 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 174 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 175 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 176 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 177 | github.com/slack-go/slack v0.9.5 h1:j7uOUDowybWf9eSgZg/AbGx6J1OPJB6SE8Z5dNl6Mtw= 178 | github.com/slack-go/slack v0.9.5/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= 179 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 180 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 181 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 182 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 183 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 184 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 185 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 186 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 187 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 188 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 189 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 190 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 191 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 192 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 193 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 194 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 195 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 196 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 197 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 198 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 199 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 200 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 201 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 202 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 203 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 204 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 205 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 206 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 207 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 208 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 209 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 210 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 211 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 212 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 213 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 214 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 215 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 216 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 217 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 218 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 219 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 220 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 221 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 222 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 223 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 224 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 225 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 226 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 227 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 228 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 229 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 230 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 231 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 232 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 233 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 234 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 235 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 236 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 237 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 238 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 239 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 240 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 241 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 242 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 243 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 244 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 246 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 247 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 248 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 249 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 250 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 251 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 252 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 253 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 254 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 255 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 256 | golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= 257 | golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 258 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 259 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 260 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 261 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 262 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 263 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 264 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 265 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 266 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 267 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 268 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 269 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 270 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 271 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 272 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 273 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 274 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 275 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 276 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 277 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 278 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 279 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 280 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= 300 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 301 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 302 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 303 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= 304 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 305 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 306 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 307 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 308 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 309 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 310 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 311 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 312 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 313 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 314 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 315 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= 316 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 317 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 318 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 319 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 320 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 321 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 322 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 323 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 324 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 325 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 326 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 327 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 328 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 329 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 330 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 331 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 332 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 333 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 334 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 335 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 336 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 337 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 338 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 339 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 340 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 341 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 342 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 343 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 344 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 345 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 346 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 347 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 348 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 349 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 350 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 351 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 352 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 353 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 354 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 355 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 356 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 357 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 358 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 359 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 360 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 361 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 362 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 363 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 364 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 365 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 366 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 367 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 368 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 369 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 370 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 371 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 372 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 373 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 374 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 375 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 376 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 377 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 378 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 379 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 380 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 381 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 382 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 383 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 384 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 385 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 386 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 387 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 388 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 389 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 390 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 391 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 392 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 393 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 394 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 395 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 396 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 397 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 398 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 399 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 400 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 401 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 402 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 403 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 404 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 405 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 406 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 407 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 408 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 409 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 410 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 411 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 412 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 413 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 414 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 415 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 416 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 417 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 418 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 419 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 420 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 421 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 422 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 423 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 424 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 425 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 426 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 427 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 428 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 429 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 430 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 431 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 432 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 433 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 434 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 435 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 436 | k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= 437 | k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= 438 | k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= 439 | k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= 440 | k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= 441 | k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= 442 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 443 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 444 | k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= 445 | k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= 446 | k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= 447 | k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= 448 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= 449 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 450 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 451 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 452 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 453 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 454 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= 455 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= 456 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 457 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 458 | -------------------------------------------------------------------------------- /hack/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: kubediff 6 | --- 7 | # Configmap 8 | apiVersion: v1 9 | kind: ConfigMap 10 | metadata: 11 | name: kubediff-configmap 12 | namespace: kubediff 13 | labels: 14 | app: kubediff 15 | data: 16 | config.yaml: | 17 | ## Resources you want to watch 18 | resources: 19 | - kind: v1/pods # Name of the resource. Resource name must be in group/version/resource (G/V/R) format 20 | # resource name should be plural (e.g apps/v1/deployments, v1/pods) 21 | - kind: v1/services 22 | - kind: apps/v1/deployments 23 | - kind: apps/v1/statefulsets 24 | - kind: networking.k8s.io/v1beta1/ingresses 25 | - kind: v1/nodes 26 | - kind: v1/namespaces 27 | - kind: v1/persistentvolumes 28 | - kind: v1/persistentvolumeclaims 29 | - kind: v1/configmaps 30 | - kind: apps/v1/daemonsets 31 | - kind: batch/v1/jobs 32 | - kind: rbac.authorization.k8s.io/v1/roles 33 | - kind: rbac.authorization.k8s.io/v1/rolebindings 34 | - kind: rbac.authorization.k8s.io/v1/clusterrolebindings 35 | - kind: rbac.authorization.k8s.io/v1/clusterroles 36 | 37 | namespaces: 38 | - all 39 | 40 | --- 41 | apiVersion: v1 42 | kind: ServiceAccount 43 | metadata: 44 | name: kubediff-sa 45 | namespace: kubediff 46 | labels: 47 | app: kubediff 48 | --- 49 | apiVersion: rbac.authorization.k8s.io/v1 50 | kind: ClusterRole 51 | metadata: 52 | name: kubediff-clusterrole 53 | labels: 54 | app: kubediff 55 | rules: 56 | - apiGroups: ["*"] 57 | resources: ["*"] 58 | verbs: ["get", "watch", "list"] 59 | --- 60 | apiVersion: rbac.authorization.k8s.io/v1 61 | kind: ClusterRoleBinding 62 | metadata: 63 | name: kubediff-clusterrolebinding 64 | labels: 65 | app: kubediff 66 | roleRef: 67 | apiGroup: rbac.authorization.k8s.io 68 | kind: ClusterRole 69 | name: kubediff-clusterrole 70 | subjects: 71 | - kind: ServiceAccount 72 | name: kubediff-sa 73 | namespace: kubediff 74 | --- 75 | apiVersion: apps/v1 76 | kind: Deployment 77 | metadata: 78 | name: kubediff 79 | namespace: kubediff 80 | labels: 81 | component: controller 82 | app: kubediff 83 | spec: 84 | replicas: 1 85 | selector: 86 | matchLabels: 87 | component: controller 88 | app: kubediff 89 | template: 90 | metadata: 91 | labels: 92 | component: controller 93 | app: kubediff 94 | spec: 95 | serviceAccountName: kubediff-sa 96 | containers: 97 | - name: kubediff 98 | image: "docker.io/arriqaaq/kubediff:v0.0.5" 99 | imagePullPolicy: Always 100 | args: 101 | - --config=/config/ 102 | env: 103 | - name: LOG_LEVEL 104 | value: "info" 105 | volumeMounts: 106 | - name: config-volume 107 | mountPath: "/config" 108 | volumes: 109 | - name: config-volume 110 | configMap: 111 | name: kubediff-configmap 112 | -------------------------------------------------------------------------------- /hack/release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | version=$(cut -d'=' -f2- .release) 4 | if [[ -z ${version} ]]; then 5 | echo "Invalid version set in .release" 6 | exit 1 7 | fi 8 | 9 | 10 | if [[ -z ${GITHUB_TOKEN} ]]; then 11 | echo "GITHUB_TOKEN not set. Usage: GITHUB_TOKEN= ./hack/release.sh" 12 | exit 1 13 | fi 14 | 15 | echo "Publishing release ${version}" 16 | 17 | generate_changelog() { 18 | local version=$1 19 | 20 | # generate changelog from github 21 | git-chglog --output CHANGELOG.md 22 | sed -i '' '$d' CHANGELOG.md 23 | } 24 | 25 | update_chart_yamls() { 26 | local version=$1 27 | 28 | sed -i '' "s/version.*/version: ${version}/" helm/kubediff/Chart.yaml 29 | sed -i '' "s/appVersion.*/appVersion: ${version}/" helm/kubediff/Chart.yaml 30 | sed -i '' "s/\btag:.*/tag: ${version}/" helm/kubediff/values.yaml 31 | sed -i '' "s/\bimage: \"arriqaaq\/kubediff.*\b/image: \"arriqaaq\/kubediff:${version}/g" hack/deploy.yaml 32 | } 33 | 34 | publish_release() { 35 | local version=$1 36 | 37 | github-release release \ 38 | --user arriqaaq \ 39 | --repo kubediff \ 40 | --tag $version \ 41 | --name "$version" \ 42 | --description "$version" 43 | } 44 | 45 | update_chart_yamls $version 46 | generate_changelog $version 47 | make release 48 | publish_release $version 49 | 50 | echo "Release ${version} published." 51 | -------------------------------------------------------------------------------- /helm/kubediff/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /helm/kubediff/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: kubediff 3 | version: v0.0.1 4 | appVersion: v0.0.1 5 | description: A kubernetes resource diff logger with the ability to send event notifications to webhook/slack. 6 | sources: 7 | - https://github.com/arriqaaq/kubediff 8 | keywords: 9 | - kubediff 10 | - kubernetes 11 | - kubernetes-monitoring 12 | - kubernetes-controller 13 | -------------------------------------------------------------------------------- /helm/kubediff/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "kubediff.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "kubediff.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "kubediff.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Create the name of the service account to use 36 | */}} 37 | {{- define "kubediff.serviceAccountName" -}} 38 | {{- if .Values.serviceAccount.create -}} 39 | {{ include "kubediff.fullname" . }}-sa 40 | {{- else -}} 41 | {{ default "default" .Values.serviceAccount.name }} 42 | {{- end -}} 43 | {{- end -}} 44 | -------------------------------------------------------------------------------- /helm/kubediff/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "kubediff.fullname" . }}-clusterrole 6 | labels: 7 | app.kubernetes.io/name: {{ include "kubediff.name" . }} 8 | helm.sh/chart: {{ include "kubediff.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | rules: 12 | {{- with .Values.rbac.rules }} 13 | {{- toYaml . | nindent 2 }} 14 | {{- end }} 15 | {{ end }} 16 | -------------------------------------------------------------------------------- /helm/kubediff/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "kubediff.fullname" . }}-clusterrolebinding 6 | labels: 7 | app.kubernetes.io/name: {{ include "kubediff.name" . }} 8 | helm.sh/chart: {{ include "kubediff.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: {{ include "kubediff.fullname" . }}-clusterrole 15 | subjects: 16 | - kind: ServiceAccount 17 | name: {{ include "kubediff.serviceAccountName" . }} 18 | namespace: {{ .Release.Namespace }} 19 | {{ end }} 20 | -------------------------------------------------------------------------------- /helm/kubediff/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "kubediff.fullname" . }}-configmap 5 | labels: 6 | app.kubernetes.io/name: {{ include "kubediff.name" . }} 7 | helm.sh/chart: {{ include "kubediff.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | data: 11 | config.yaml: | 12 | {{- with .Values.config }} 13 | {{- toYaml . | nindent 4 }} 14 | {{- end }} 15 | 16 | -------------------------------------------------------------------------------- /helm/kubediff/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "kubediff.fullname" . }} 5 | labels: 6 | app.kubernetes.io/name: {{ include "kubediff.name" . }} 7 | helm.sh/chart: {{ include "kubediff.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | component: controller 11 | app: kubediff 12 | spec: 13 | replicas: {{ .Values.replicaCount }} 14 | selector: 15 | matchLabels: 16 | component: controller 17 | app: kubediff 18 | template: 19 | metadata: 20 | labels: 21 | component: controller 22 | app: kubediff 23 | annotations: 24 | checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 25 | {{- if .Values.extraAnnotations }} 26 | {{ toYaml .Values.extraAnnotations | indent 8 }} 27 | {{- end }} 28 | spec: 29 | {{- if .Values.priorityClassName }} 30 | priorityClassName: "{{ .Values.priorityClassName }}" 31 | {{- end }} 32 | serviceAccountName: {{ include "kubediff.serviceAccountName" . }} 33 | {{- if .Values.image.pullSecrets }} 34 | imagePullSecrets: 35 | {{- range .Values.image.pullSecrets }} 36 | - name: {{ . }} 37 | {{- end }} 38 | {{- end }} 39 | containers: 40 | - name: {{ .Chart.Name }} 41 | image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" 42 | imagePullPolicy: {{ .Values.image.pullPolicy }} 43 | {{- if .Values.containerSecurityContext }} 44 | securityContext: 45 | {{- toYaml .Values.containerSecurityContext | nindent 12 }} 46 | {{ end }} 47 | args: 48 | - -config=/config/ 49 | env: 50 | - name: LOG_LEVEL 51 | value: {{ .Values.logLevel | quote }} 52 | volumeMounts: 53 | - name: config-volume 54 | mountPath: "/config" 55 | {{- if .Values.resources }} 56 | resources: 57 | {{ toYaml .Values.resources | indent 12 }} 58 | {{- end }} 59 | volumes: 60 | - name: config-volume 61 | configMap: 62 | name: {{ include "kubediff.fullname" . }}-configmap 63 | {{- if .Values.securityContext }} 64 | securityContext: 65 | runAsUser: {{ .Values.securityContext.runAsUser }} 66 | runAsGroup: {{ .Values.securityContext.runAsGroup }} 67 | {{ end }} 68 | {{- with .Values.nodeSelector }} 69 | nodeSelector: 70 | {{- toYaml . | nindent 8 }} 71 | {{- end }} 72 | {{- if .Values.tolerations }} 73 | tolerations: 74 | {{- toYaml .Values.tolerations | nindent 8 }} 75 | {{- end }} 76 | 77 | -------------------------------------------------------------------------------- /helm/kubediff/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "kubediff.serviceAccountName" . }} 5 | {{- if .Values.serviceAccount.annotations }} 6 | annotations: 7 | {{ toYaml .Values.serviceAccount.annotations | indent 4 }} 8 | {{- end }} 9 | labels: 10 | app.kubernetes.io/name: {{ include "kubediff.name" . }} 11 | helm.sh/chart: {{ include "kubediff.chart" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | app.kubernetes.io/managed-by: {{ .Release.Service }} 14 | -------------------------------------------------------------------------------- /helm/kubediff/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for kubediff. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | # Extra annotations to pass to the kubediff pod 7 | extraAnnotations: {} 8 | # Priority class name for the pod 9 | priorityClassName: "" 10 | image: 11 | registry: docker.io 12 | repository: arriqaaq/kubediff 13 | pullPolicy: IfNotPresent 14 | ## default tag is appVersion from Chart.yaml. If you want to use 15 | ## some other tag then it can be specified here 16 | tag: v0.0.5 17 | 18 | nameOverride: "" 19 | fullnameOverride: "" 20 | 21 | # Enable podSecurityPolicy to allow kubediff to run in restricted clusters 22 | podSecurityPolicy: 23 | enabled: false 24 | 25 | # Configure securityContext to manage user Privileges in pods 26 | # set to run as a Non-Privileged user by default 27 | securityContext: 28 | runAsUser: 101 29 | runAsGroup: 101 30 | 31 | containerSecurityContext: 32 | privileged: false 33 | allowPrivilegeEscalation: false 34 | readOnlyRootFilesystem: true 35 | 36 | # set one of the log levels- info, warn, debug, error, fatal, panic 37 | logLevel: info 38 | 39 | config: 40 | ## Resources you want to watch 41 | resources: 42 | - kind: v1/pods # Name of the resource. Resource name must be in group/version/resource (G/V/R) format 43 | # resource name should be plural (e.g apps/v1/deployments, v1/pods) 44 | - kind: v1/services 45 | - kind: apps/v1/deployments 46 | - kind: apps/v1/statefulsets 47 | - kind: networking.k8s.io/v1beta1/ingresses 48 | - kind: v1/nodes 49 | - kind: v1/namespaces 50 | - kind: v1/persistentvolumes 51 | - kind: v1/persistentvolumeclaims 52 | - kind: v1/configmaps 53 | - kind: apps/v1/daemonsets 54 | - kind: batch/v1/jobs 55 | - kind: rbac.authorization.k8s.io/v1/roles 56 | - kind: rbac.authorization.k8s.io/v1/rolebindings 57 | - kind: rbac.authorization.k8s.io/v1/clusterrolebindings 58 | - kind: rbac.authorization.k8s.io/v1/clusterroles 59 | 60 | namespaces: 61 | - all 62 | 63 | # notifier settings 64 | notifier: 65 | slack: 66 | enabled: false 67 | channel: 'SLACK_CHANNEL' 68 | token: 'SLACK_API_TOKEN' 69 | title: 'kubediff event' 70 | webhook: 71 | enabled: false 72 | url: 'WEBHOOK_URL' 73 | 74 | 75 | resources: {} 76 | # We usually recommend not to specify default resources and to leave this as a conscious 77 | # choice for the user. This also increases chances charts run on environments with little 78 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 79 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 80 | # limits: 81 | # cpu: 100m 82 | # memory: 128Mi 83 | # requests: 84 | # cpu: 100m 85 | # memory: 128Mi 86 | 87 | nodeSelector: {} 88 | 89 | tolerations: [] 90 | 91 | affinity: {} 92 | 93 | rbac: 94 | create: true 95 | rules: 96 | - apiGroups: ["*"] 97 | resources: ["*"] 98 | verbs: ["get", "watch", "list"] 99 | 100 | serviceAccount: 101 | # Specifies whether a service account should be created 102 | create: true 103 | # The name of the service account to use. 104 | # If not set and create is true, a name is generated using the fullname template 105 | #name: 106 | # annotations for the service account 107 | annotations: {} -------------------------------------------------------------------------------- /kubediff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arriqaaq/kubediff/a9d1958f81a27c3aaca4f0cd174f90c4b95ac7e9/kubediff.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "syscall" 10 | 11 | "github.com/arriqaaq/kubediff/config" 12 | "github.com/arriqaaq/kubediff/pkg/watcher" 13 | ) 14 | 15 | var ( 16 | configFilePath = "config.yaml" 17 | configPath = flag.String("config", "", "config folder path") 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | filepath := filepath.Join(*configPath, configFilePath) 23 | conf, err := config.New(filepath) 24 | if err != nil { 25 | log.Fatalf("Error in loading configuration. Error:%s", err.Error()) 26 | } 27 | 28 | watcher, err := watcher.NewWatcher(conf) 29 | if err != nil { 30 | log.Fatalf("Error in loading configuration. Error:%s", err.Error()) 31 | } 32 | 33 | stopCh := make(chan struct{}) 34 | defer close(stopCh) 35 | 36 | watcher.Run(stopCh) 37 | 38 | sigterm := make(chan os.Signal, 1) 39 | signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGSTOP) 40 | 41 | <-sigterm 42 | } 43 | -------------------------------------------------------------------------------- /pkg/event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type Event struct { 4 | Type string 5 | Kind string 6 | Obj interface{} 7 | Diff interface{} 8 | } 9 | 10 | func NewEvent(event string, kind string, obj interface{}, diff interface{}) Event { 11 | return Event{event, kind, obj, diff} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | var log = logrus.New() 10 | 11 | func init() { 12 | log.SetOutput(os.Stdout) 13 | 14 | logLevel, err := logrus.ParseLevel(os.Getenv("LOG_LEVEL")) 15 | if err != nil { 16 | logLevel = logrus.InfoLevel 17 | } 18 | log.SetLevel(logLevel) 19 | log.Formatter = &logrus.JSONFormatter{ 20 | PrettyPrint: true, 21 | TimestampFormat: "2006-01-02 15:04:05", 22 | } 23 | } 24 | 25 | func Info(message ...interface{}) { 26 | log.Info(message...) 27 | } 28 | 29 | func Trace(message ...interface{}) { 30 | log.Trace(message...) 31 | } 32 | 33 | func Debug(message ...interface{}) { 34 | log.Debug(message...) 35 | } 36 | 37 | func Warn(message ...interface{}) { 38 | log.Warn(message...) 39 | } 40 | 41 | func Error(message ...interface{}) { 42 | log.Error(message...) 43 | } 44 | 45 | func Fatal(message ...interface{}) { 46 | log.Fatal(message...) 47 | } 48 | 49 | func Panic(message ...interface{}) { 50 | log.Panic(message...) 51 | } 52 | 53 | func Infof(format string, v ...interface{}) { 54 | log.Infof(format, v...) 55 | } 56 | 57 | func Tracef(format string, v ...interface{}) { 58 | log.Tracef(format, v...) 59 | } 60 | 61 | func Debugf(format string, v ...interface{}) { 62 | log.Debugf(format, v...) 63 | } 64 | 65 | func Warnf(format string, v ...interface{}) { 66 | log.Warnf(format, v...) 67 | } 68 | 69 | func Errorf(format string, v ...interface{}) { 70 | log.Errorf(format, v...) 71 | } 72 | 73 | func Fatalf(format string, v ...interface{}) { 74 | log.Fatalf(format, v...) 75 | } 76 | 77 | func Panicf(format string, v ...interface{}) { 78 | log.Panicf(format, v...) 79 | } 80 | 81 | func WithField(key string, value interface{}) *logrus.Entry { 82 | return log.WithField(key, value) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/notify/noop.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/arriqaaq/kubediff/config" 7 | "github.com/arriqaaq/kubediff/pkg/event" 8 | ) 9 | 10 | var _ Notifier = &NoOp{} 11 | 12 | type NoOp struct { 13 | } 14 | 15 | func NewNoOp(c *config.Config) Notifier { 16 | return &NoOp{} 17 | } 18 | 19 | func (s *NoOp) Handle(e event.Event) error { 20 | log.Printf("Woah! Message successfully sent %+v\n ", e) 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /pkg/notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "github.com/arriqaaq/kubediff/config" 5 | "github.com/arriqaaq/kubediff/pkg/event" 6 | "k8s.io/apimachinery/pkg/util/errors" 7 | ) 8 | 9 | type Notifier interface { 10 | Handle(e event.Event) error 11 | } 12 | 13 | type NotifierList struct { 14 | notifiers []Notifier 15 | } 16 | 17 | func (n *NotifierList) Handle(e event.Event) error { 18 | 19 | errs := make([]error, 0, len(n.notifiers)) 20 | for _, n := range n.notifiers { 21 | if err := n.Handle(e); err != nil { 22 | errs = append(errs, err) 23 | } 24 | } 25 | if len(errs) > 0 { 26 | return errors.NewAggregate(errs) 27 | } 28 | return nil 29 | } 30 | 31 | func NewNotifierList(conf *config.Config) Notifier { 32 | var notifiers []Notifier 33 | if conf.Notifier.Slack.Enabled { 34 | notifiers = append(notifiers, NewSlack(conf)) 35 | } 36 | if conf.Notifier.Webhook.Enabled { 37 | notifiers = append(notifiers, NewWebhook(conf)) 38 | } 39 | if conf.Notifier.NoOp.Enabled { 40 | notifiers = append(notifiers, NewNoOp(conf)) 41 | } 42 | 43 | return &NotifierList{notifiers} 44 | } 45 | -------------------------------------------------------------------------------- /pkg/notify/slack.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | "github.com/arriqaaq/kubediff/config" 8 | "github.com/arriqaaq/kubediff/pkg/event" 9 | "github.com/slack-go/slack" 10 | ) 11 | 12 | var _ Notifier = &Slack{} 13 | 14 | type Slack struct { 15 | Token string 16 | Channel string 17 | Title string 18 | } 19 | 20 | func NewSlack(c *config.Config) Notifier { 21 | return &Slack{ 22 | Token: c.Notifier.Slack.Token, 23 | Channel: c.Notifier.Slack.Channel, 24 | Title: c.Notifier.Slack.Title, 25 | } 26 | } 27 | 28 | // Handle handles the notification. 29 | func (s *Slack) Handle(e event.Event) error { 30 | api := slack.New(s.Token) 31 | 32 | message, err := json.Marshal(e) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | attachment := prepareSlackAttachment(string(message), s) 38 | 39 | channelID, timestamp, err := api.PostMessage(s.Channel, 40 | slack.MsgOptionAttachments(attachment), 41 | slack.MsgOptionAsUser(true)) 42 | if err != nil { 43 | log.Printf("%s\n", err) 44 | return err 45 | } 46 | 47 | log.Printf("Message successfully sent to channel %s at %s", channelID, timestamp) 48 | return nil 49 | } 50 | 51 | func prepareSlackAttachment(e string, s *Slack) slack.Attachment { 52 | 53 | attachment := slack.Attachment{ 54 | Fields: []slack.AttachmentField{ 55 | { 56 | Title: s.Title, 57 | Value: e, 58 | }, 59 | }, 60 | MarkdownIn: []string{"fields"}, 61 | } 62 | 63 | return attachment 64 | } 65 | -------------------------------------------------------------------------------- /pkg/notify/webhook.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/arriqaaq/kubediff/config" 11 | "github.com/arriqaaq/kubediff/pkg/event" 12 | "github.com/arriqaaq/kubediff/pkg/log" 13 | ) 14 | 15 | var _ Notifier = &Webhook{} 16 | 17 | type Webhook struct { 18 | URL string 19 | } 20 | 21 | type payload struct { 22 | Event event.Event `json:"meta"` 23 | TimeStamp time.Time `json:"timestamp"` 24 | } 25 | 26 | func NewWebhook(c *config.Config) Notifier { 27 | return &Webhook{ 28 | URL: c.Notifier.Webhook.Url, 29 | } 30 | } 31 | 32 | func (w *Webhook) Handle(event event.Event) (err error) { 33 | pl := &payload{ 34 | Event: event, 35 | TimeStamp: time.Now(), 36 | } 37 | 38 | err = w.post(pl) 39 | if err != nil { 40 | log.Error(err) 41 | log.Debugf("error sending event to webhook %v", event) 42 | } 43 | 44 | log.Debugf("Event successfully sent to Webhook %v", event) 45 | return nil 46 | } 47 | 48 | func (w *Webhook) post(pl *payload) error { 49 | 50 | message, err := json.Marshal(pl) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | req, err := http.NewRequest("POST", w.URL, bytes.NewBuffer(message)) 56 | if err != nil { 57 | return err 58 | } 59 | req.Header.Add("Content-Type", "application/json") 60 | 61 | client := &http.Client{} 62 | resp, err := client.Do(req) 63 | if err != nil { 64 | return err 65 | } 66 | if resp.StatusCode != http.StatusOK { 67 | return fmt.Errorf("error response from webhook: %s", fmt.Sprint(resp.StatusCode)) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/watcher/client.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "k8s.io/client-go/discovery" 5 | "k8s.io/client-go/discovery/cached/memory" 6 | "k8s.io/client-go/dynamic" 7 | "k8s.io/client-go/rest" 8 | "k8s.io/client-go/restmapper" 9 | ) 10 | 11 | type Client struct { 12 | discoveryClient *discovery.DiscoveryClient 13 | discoveryMapper *restmapper.DeferredDiscoveryRESTMapper 14 | dynamicClient dynamic.Interface 15 | } 16 | 17 | func newClient(config *rest.Config) (*Client, error) { 18 | 19 | disC, err := discovery.NewDiscoveryClientForConfig(config) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | dynC, err := dynamic.NewForConfig(config) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | cacheC := memory.NewMemCacheClient(disC) 30 | cacheC.Invalidate() 31 | 32 | dm := restmapper.NewDeferredDiscoveryRESTMapper(cacheC) 33 | 34 | return &Client{disC, dm, dynC}, nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/watcher/handlers.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "github.com/arriqaaq/kubediff/config" 5 | "github.com/arriqaaq/kubediff/pkg/event" 6 | "github.com/arriqaaq/kubediff/pkg/log" 7 | "github.com/arriqaaq/kubediff/pkg/notify" 8 | "github.com/go-test/deep" 9 | "k8s.io/apimachinery/pkg/api/equality" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/client-go/tools/cache" 12 | ) 13 | 14 | const ( 15 | EventAdd string = "EventAdd" 16 | EventUpdate string = "EventUpdate" 17 | EventDelete string = "EventDelete" 18 | ) 19 | 20 | type eventHandler func(resourceType string, notifier notify.Notifier) cache.ResourceEventHandlerFuncs 21 | 22 | func watchHandler(resourceType string, notifier notify.Notifier) cache.ResourceEventHandlerFuncs { 23 | 24 | var handler cache.ResourceEventHandlerFuncs 25 | handler.AddFunc = func(obj interface{}) { 26 | log.WithField("resourceType", resourceType).WithField("obj", obj).Info("add event") 27 | notifier.Handle(event.NewEvent(EventAdd, resourceType, obj, nil)) 28 | } 29 | handler.UpdateFunc = func(old, new interface{}) { 30 | log.WithField("resourceType", resourceType).WithField("old", old).WithField("new", new).Info("update event") 31 | notifier.Handle(event.NewEvent(EventUpdate, resourceType, new, nil)) 32 | } 33 | handler.DeleteFunc = func(obj interface{}) { 34 | log.WithField("resourceType", resourceType).WithField("obj", obj).Info("delete event") 35 | notifier.Handle(event.NewEvent(EventDelete, resourceType, obj, nil)) 36 | } 37 | return handler 38 | } 39 | 40 | func diffHandler(resourceType string, notifier notify.Notifier) cache.ResourceEventHandlerFuncs { 41 | 42 | var handler cache.ResourceEventHandlerFuncs 43 | handler.UpdateFunc = func(old, new interface{}) { 44 | oldObj := old.(*unstructured.Unstructured) 45 | newObj := new.(*unstructured.Unstructured) 46 | 47 | if !equality.Semantic.DeepEqual(old, new) { 48 | diff := deep.Equal(oldObj, newObj) 49 | log.WithField("resourceType", resourceType).WithField("diff", diff).Info("update event") 50 | notifier.Handle(event.NewEvent(EventUpdate, resourceType, old, diff)) 51 | } 52 | } 53 | return handler 54 | } 55 | 56 | func noOpHandler(resourceType string, notifier notify.Notifier) cache.ResourceEventHandlerFuncs { 57 | 58 | var handler cache.ResourceEventHandlerFuncs 59 | handler.AddFunc = func(obj interface{}) { 60 | log.WithField("resourceType", resourceType).Info("delete event") 61 | } 62 | handler.UpdateFunc = func(old, new interface{}) { 63 | log.WithField("resourceType", resourceType).Info("delete event") 64 | } 65 | handler.DeleteFunc = func(obj interface{}) { 66 | log.WithField("resourceType", resourceType).Info("delete event") 67 | } 68 | return handler 69 | } 70 | 71 | func getEventHandler(mode config.RunMode) eventHandler { 72 | switch mode { 73 | case config.DiffMode: 74 | return diffHandler 75 | default: 76 | return watchHandler 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/watcher/informer.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/arriqaaq/kubediff/config" 7 | "github.com/arriqaaq/kubediff/pkg/notify" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/client-go/dynamic/dynamicinformer" 10 | "k8s.io/client-go/tools/cache" 11 | ) 12 | 13 | type Informer interface { 14 | AddEventHandler(handler eventHandler, notifier notify.Notifier) 15 | HasSynced() bool 16 | Start(ch <-chan struct{}) 17 | } 18 | 19 | type NewInformerFunc func(client *Client) (*multiResourceInformer, error) 20 | 21 | func NewMultiResourceInformer(cfg *config.Config, resyncPeriod time.Duration) NewInformerFunc { 22 | return func(client *Client) (*multiResourceInformer, error) { 23 | informers := make(map[string]map[string]cache.SharedIndexInformer) 24 | 25 | resources := make(map[string]schema.GroupVersionResource) 26 | for _, r := range cfg.Resources { 27 | gvr, err := getGVRFromResource(client.discoveryMapper, r.Kind) 28 | if err != nil { 29 | return nil, err 30 | } 31 | resources[r.Kind] = gvr 32 | } 33 | 34 | dynamicInformers := make([]dynamicinformer.DynamicSharedInformerFactory, 0, len(cfg.Namespaces)) 35 | 36 | for _, ns := range cfg.Namespaces { 37 | 38 | namespace := getNamespace(ns) 39 | di := dynamicinformer.NewFilteredDynamicSharedInformerFactory( 40 | client.dynamicClient, 41 | resyncPeriod, 42 | namespace, 43 | nil, 44 | ) 45 | 46 | for r, gvr := range resources { 47 | if _, ok := informers[ns]; !ok { 48 | informers[ns] = make(map[string]cache.SharedIndexInformer) 49 | } 50 | informers[ns][r] = di.ForResource(gvr).Informer() 51 | } 52 | 53 | dynamicInformers = append(dynamicInformers, di) 54 | } 55 | 56 | return &multiResourceInformer{ 57 | resourceToGVR: resources, 58 | resourceToInformer: informers, 59 | informerFactory: dynamicInformers, 60 | }, nil 61 | } 62 | } 63 | 64 | type multiResourceInformer struct { 65 | resourceToGVR map[string]schema.GroupVersionResource 66 | resourceToInformer map[string]map[string]cache.SharedIndexInformer 67 | informerFactory []dynamicinformer.DynamicSharedInformerFactory 68 | } 69 | 70 | var _ Informer = &multiResourceInformer{} 71 | 72 | // AddEventHandler adds the handler to each namespaced informer 73 | func (i *multiResourceInformer) AddEventHandler(handler eventHandler, notifier notify.Notifier) { 74 | for _, ki := range i.resourceToInformer { 75 | for kind, informer := range ki { 76 | informer.AddEventHandler(handler(kind, notifier)) 77 | } 78 | } 79 | } 80 | 81 | // HasSynced checks if each namespaced informer has synced 82 | func (i *multiResourceInformer) HasSynced() bool { 83 | for _, ki := range i.resourceToInformer { 84 | for _, informer := range ki { 85 | if ok := informer.HasSynced(); !ok { 86 | return ok 87 | } 88 | } 89 | } 90 | 91 | return true 92 | } 93 | 94 | func (i *multiResourceInformer) Start(stopCh <-chan struct{}) { 95 | for _, informer := range i.informerFactory { 96 | informer.Start(stopCh) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/watcher/utils.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/client-go/rest" 10 | "k8s.io/client-go/restmapper" 11 | "k8s.io/client-go/tools/clientcmd" 12 | ) 13 | 14 | func newKubeConfig() (*rest.Config, error) { 15 | var config *rest.Config 16 | var err error 17 | 18 | config, err = rest.InClusterConfig() 19 | if err != nil { 20 | kubeconfigPath := os.Getenv("KUBECONFIG") 21 | if kubeconfigPath == "" { 22 | kubeconfigPath = os.Getenv("HOME") + "/.kube/config" 23 | } 24 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) 25 | if err != nil { 26 | return nil, err 27 | } 28 | } 29 | return config, nil 30 | } 31 | 32 | func getGVRFromResource(disco *restmapper.DeferredDiscoveryRESTMapper, resource string) (schema.GroupVersionResource, error) { 33 | var gvr schema.GroupVersionResource 34 | if strings.Count(resource, "/") >= 2 { 35 | s := strings.SplitN(resource, "/", 3) 36 | gvr = schema.GroupVersionResource{Group: s[0], Version: s[1], Resource: s[2]} 37 | } else if strings.Count(resource, "/") == 1 { 38 | s := strings.SplitN(resource, "/", 2) 39 | gvr = schema.GroupVersionResource{Group: "", Version: s[0], Resource: s[1]} 40 | } 41 | 42 | if _, err := disco.ResourcesFor(gvr); err != nil { 43 | return schema.GroupVersionResource{}, err 44 | } 45 | return gvr, nil 46 | } 47 | 48 | func getNamespace(ns string) string { 49 | switch ns { 50 | case "all": 51 | return metav1.NamespaceAll 52 | default: 53 | return ns 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/arriqaaq/kubediff/config" 7 | "github.com/arriqaaq/kubediff/pkg/notify" 8 | ) 9 | 10 | const ( 11 | resyncPeriod = time.Duration(1) * time.Minute 12 | ) 13 | 14 | func NewWatcher(cfg *config.Config) (*Watcher, error) { 15 | kubeconfig, err := newKubeConfig() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | client, err := newClient(kubeconfig) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | informer, err := NewMultiResourceInformer(cfg, resyncPeriod)(client) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | notifier := notify.NewNotifierList(cfg) 31 | informer.AddEventHandler(getEventHandler(cfg.Mode), notifier) 32 | 33 | return &Watcher{client, informer}, nil 34 | } 35 | 36 | type Watcher struct { 37 | client *Client 38 | informer Informer 39 | } 40 | 41 | func (w *Watcher) Run(stopCh chan struct{}) { 42 | w.informer.Start(stopCh) 43 | } 44 | -------------------------------------------------------------------------------- /test/config.yaml: -------------------------------------------------------------------------------- 1 | mode: watch 2 | resources: 3 | - kind: v1/configmaps 4 | namespaces: 5 | - all 6 | notifier: 7 | noop: 8 | enabled: true --------------------------------------------------------------------------------