├── .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 | [](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
--------------------------------------------------------------------------------