├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bors.toml ├── cmd ├── config.go ├── flock.go ├── hipchat.go ├── mattermost.go ├── msteams.go ├── resource.go ├── root.go ├── slack.go ├── smtp.go ├── version.go └── webhook.go ├── config ├── config.go ├── config_test.go └── sample.go ├── docs ├── CONTRIBUTION.md ├── design.md ├── kubewatch-logo.jpeg ├── kubewatch.png └── slack.png ├── examples └── conf │ ├── kubewatch.conf.flock.yaml │ ├── kubewatch.conf.hipchat.yaml │ ├── kubewatch.conf.json │ ├── kubewatch.conf.mattermost.yaml │ ├── kubewatch.conf.msteams.yaml │ └── kubewatch.conf.webhook.yaml ├── go.mod ├── go.sum ├── kubewatch-configmap.yaml ├── kubewatch-in-cluster.yaml ├── kubewatch-service-account.yaml ├── kubewatch.yaml ├── main.go ├── pkg ├── client │ └── run.go ├── controller │ └── controller.go ├── event │ └── event.go ├── handlers │ ├── flock │ │ ├── flock.go │ │ └── flock_test.go │ ├── handler.go │ ├── hipchat │ │ ├── hipchat.go │ │ └── hipchat_test.go │ ├── mattermost │ │ ├── mattermost.go │ │ └── mattermost_test.go │ ├── msteam │ │ ├── msteam.go │ │ └── msteam_test.go │ ├── slack │ │ ├── slack.go │ │ └── slack_test.go │ ├── smtp │ │ ├── client.go │ │ ├── smtp.go │ │ └── smtp_test.go │ └── webhook │ │ ├── webhook.go │ │ └── webhook_test.go └── utils │ └── k8sutil.go └── tools └── yannotated ├── yannotated.go └── yannotated_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | kubewatch 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | branches: 3 | only: 4 | - master 5 | - /^v\d+\.\d+\.\d+.*/ 6 | - staging 7 | - trying 8 | 9 | cache: 10 | directories: 11 | - $GOPATH/pkg 12 | - $GOPATH/bin 13 | 14 | go: 15 | - 1.14.x 16 | 17 | language: go 18 | 19 | notifications: 20 | email: 21 | on_success: never 22 | 23 | os: 24 | - linux 25 | - osx 26 | 27 | sudo: required 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang AS builder 2 | MAINTAINER "Cuong Manh Le " 3 | 4 | RUN apt-get update && \ 5 | apt-get install -y --no-install-recommends build-essential && \ 6 | apt-get clean && \ 7 | mkdir -p "$GOPATH/src/github.com/bitnami-labs/kubewatch" 8 | 9 | ADD . "$GOPATH/src/github.com/bitnami-labs/kubewatch" 10 | 11 | RUN cd "$GOPATH/src/github.com/bitnami-labs/kubewatch" && \ 12 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a --installsuffix cgo --ldflags="-s" -o /kubewatch 13 | 14 | FROM bitnami/minideb:stretch 15 | RUN install_packages ca-certificates 16 | 17 | COPY --from=builder /kubewatch /bin/kubewatch 18 | 19 | ENTRYPOINT ["/bin/kubewatch"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Skippbox, Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default build docker-image test stop clean-images clean 2 | 3 | BINARY = kubewatch 4 | 5 | VERSION= 6 | BUILD= 7 | 8 | PKG = github.com/bitnami-labs/kubewatch 9 | TRAVIS_COMMIT ?= `git describe --tags` 10 | GOCMD = go 11 | BUILD_DATE = `date +%FT%T%z` 12 | GOFLAGS ?= $(GOFLAGS:) 13 | LDFLAGS := "-X '$(PKG)/cmd.gitCommit=$(TRAVIS_COMMIT)' \ 14 | -X '$(PKG)/cmd.buildDate=$(BUILD_DATE)'" 15 | 16 | default: build test 17 | 18 | build: 19 | "$(GOCMD)" build ${GOFLAGS} -ldflags ${LDFLAGS} -o "${BINARY}" 20 | 21 | docker-image: 22 | @docker build -t "${BINARY}" . 23 | 24 | test: 25 | "$(GOCMD)" test -race -v ./... 26 | 27 | stop: 28 | @docker stop "${BINARY}" 29 | 30 | clean-images: stop 31 | @docker rmi "${BUILDER}" "${BINARY}" 32 | 33 | clean: 34 | "$(GOCMD)" clean -i 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## WARNING: Kubewatch is no longer actively maintained by VMware. 2 | 3 | VMware has made the difficult decision to stop driving this project and therefore we will no longer actively respond to issues or pull requests. The project will be externally maintained in the following fork: https://github.com/robusta-dev/kubewatch 4 | 5 | Thank You. 6 | 7 |

8 | 9 |

10 | 11 | 12 | [![Build Status](https://travis-ci.org/bitnami-labs/kubewatch.svg?branch=master)](https://travis-ci.org/bitnami-labs/kubewatch) [![Go Report Card](https://goreportcard.com/badge/github.com/bitnami-labs/kubewatch)](https://goreportcard.com/report/github.com/bitnami-labs/kubewatch) [![GoDoc](https://godoc.org/github.com/bitnami-labs/kubewatch?status.svg)](https://godoc.org/github.com/bitnami-labs/kubewatch) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/bitnami-labs/kubewatch/blob/master/LICENSE) 13 | 14 | **kubewatch** is a Kubernetes watcher that currently publishes notification to available collaboration hubs/notification channels. Run it in your k8s cluster, and you will get event notifications through webhooks. 15 | 16 | # Usage 17 | ``` 18 | $ kubewatch -h 19 | 20 | Kubewatch: A watcher for Kubernetes 21 | 22 | kubewatch is a Kubernetes watcher that publishes notifications 23 | to Slack/hipchat/mattermost/flock channels. It watches the cluster 24 | for resource changes and notifies them through webhooks. 25 | 26 | supported webhooks: 27 | - slack 28 | - hipchat 29 | - mattermost 30 | - flock 31 | - webhook 32 | - smtp 33 | 34 | Usage: 35 | kubewatch [flags] 36 | kubewatch [command] 37 | 38 | Available Commands: 39 | config modify kubewatch configuration 40 | resource manage resources to be watched 41 | version print version 42 | 43 | Flags: 44 | -h, --help help for kubewatch 45 | 46 | Use "kubewatch [command] --help" for more information about a command. 47 | 48 | ``` 49 | 50 | # Install 51 | 52 | ### Cluster Installation 53 | #### Using helm: 54 | 55 | When you have helm installed in your cluster, use the following setup: 56 | 57 | ```console 58 | helm install --name kubewatch bitnami/kubewatch --set='rbac.create=true,slack.channel=#YOUR_CHANNEL,slack.token=xoxb-YOUR_TOKEN,resourcesToWatch.pod=true,resourcesToWatch.daemonset=true' 59 | ``` 60 | 61 | You may also provide a values file instead: 62 | 63 | ```yaml 64 | rbac: 65 | create: true 66 | resourcesToWatch: 67 | deployment: false 68 | replicationcontroller: false 69 | replicaset: false 70 | daemonset: false 71 | services: true 72 | pod: true 73 | job: false 74 | node: false 75 | clusterrole: true 76 | serviceaccount: true 77 | persistentvolume: false 78 | namespace: false 79 | secret: false 80 | configmap: false 81 | ingress: false 82 | slack: 83 | channel: '#YOUR_CHANNEL' 84 | token: 'xoxb-YOUR_TOKEN' 85 | ``` 86 | 87 | And use that: 88 | 89 | ```console 90 | $ helm upgrade --install kubewatch bitnami/kubewatch --values=values-file.yml 91 | ``` 92 | 93 | #### Using kubectl: 94 | 95 | In order to run kubewatch in a Kubernetes cluster quickly, the easiest way is for you to create a [ConfigMap](https://github.com/bitnami-labs/kubewatch/blob/master/kubewatch-configmap.yaml) to hold kubewatch configuration. 96 | 97 | An example is provided at [`kubewatch-configmap.yaml`](https://github.com/bitnami-labs/kubewatch/blob/master/kubewatch-configmap.yaml), do not forget to update your own slack channel and token parameters. Alternatively, you could use secrets. 98 | 99 | Create k8s configmap: 100 | 101 | ```console 102 | $ kubectl create -f kubewatch-configmap.yaml 103 | ``` 104 | 105 | Create the [Pod](https://github.com/bitnami-labs/kubewatch/blob/master/kubewatch.yaml) directly, or create your own deployment: 106 | 107 | ```console 108 | $ kubectl create -f kubewatch.yaml 109 | ``` 110 | 111 | A `kubewatch` container will be created along with `kubectl` sidecar container in order to reach the API server. 112 | 113 | Once the Pod is running, you will start seeing Kubernetes events in your configured Slack channel. Here is a screenshot: 114 | 115 | ![slack](./docs/slack.png) 116 | 117 | To modify what notifications you get, update the `kubewatch` ConfigMap and turn on and off (true/false) resources: 118 | 119 | ``` 120 | resource: 121 | deployment: false 122 | replicationcontroller: false 123 | replicaset: false 124 | daemonset: false 125 | services: true 126 | pod: true 127 | job: false 128 | node: false 129 | clusterrole: false 130 | serviceaccount: false 131 | persistentvolume: false 132 | namespace: false 133 | secret: false 134 | configmap: false 135 | ingress: false 136 | ``` 137 | 138 | #### Working with RBAC 139 | 140 | Kubernetes Engine clusters running versions 1.6 or higher introduced Role-Based Access Control (RBAC). We can create `ServiceAccount` for it to work with RBAC. 141 | 142 | ```console 143 | $ kubectl create -f kubewatch-service-account.yaml 144 | ``` 145 | 146 | If you do not have permission to create it, you need to become an admin first. For example, in GKE you would run: 147 | 148 | ``` 149 | $ kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user=REPLACE_EMAIL_HERE 150 | ``` 151 | 152 | Edit `kubewatch.yaml`, and create a new field under `spec` with `serviceAccountName: kubewatch`, you can achieve this by running: 153 | 154 | ```console 155 | $ sed -i '/spec:/a\ \ serviceAccountName: kubewatch' kubewatch.yaml 156 | ``` 157 | 158 | Then just create `pod` as usual with: 159 | 160 | ```console 161 | $ kubectl create -f kubewatch.yaml 162 | ``` 163 | 164 | ### Local Installation 165 | #### Using go package installer: 166 | 167 | ```console 168 | # Download and install kubewatch 169 | $ go get -u github.com/bitnami-labs/kubewatch 170 | 171 | # Configure the notification channel 172 | $ kubewatch config add slack --channel --token 173 | 174 | # Add resources to be watched 175 | $ kubewatch resource add --po --svc 176 | INFO[0000] resource svc configured 177 | INFO[0000] resource po configured 178 | 179 | # start kubewatch server 180 | $ kubewatch 181 | INFO[0000] Starting kubewatch controller pkg=kubewatch-service 182 | INFO[0000] Starting kubewatch controller pkg=kubewatch-pod 183 | INFO[0000] Processing add to service: default/kubernetes pkg=kubewatch-service 184 | INFO[0000] Processing add to service: kube-system/tiller-deploy pkg=kubewatch-service 185 | INFO[0000] Processing add to pod: kube-system/tiller-deploy-69ffbf64bc-h8zxm pkg=kubewatch-pod 186 | INFO[0000] Kubewatch controller synced and ready pkg=kubewatch-service 187 | INFO[0000] Kubewatch controller synced and ready pkg=kubewatch-pod 188 | 189 | ``` 190 | #### Using Docker: 191 | 192 | To Run Kubewatch Container interactively, place the config file in `$HOME/.kubewatch.yaml` location and use the following command. 193 | 194 | ``` 195 | docker run --rm -it --network host -v $HOME/.kubewatch.yaml:/root/.kubewatch.yaml -v $HOME/.kube/config:/opt/bitnami/kubewatch/.kube/config --name bitnami/kubewatch 196 | ``` 197 | 198 | Example: 199 | 200 | ``` 201 | $ docker run --rm -it --network host -v $HOME/.kubewatch.yaml:/root/.kubewatch.yaml -v $HOME/.kube/config:/opt/bitnami/kubewatch/.kube/config --name kubewatch-app bitnami/kubewatch 202 | 203 | ==> Writing config file... 204 | INFO[0000] Starting kubewatch controller pkg=kubewatch-service 205 | INFO[0000] Starting kubewatch controller pkg=kubewatch-pod 206 | INFO[0000] Starting kubewatch controller pkg=kubewatch-deployment 207 | INFO[0000] Starting kubewatch controller pkg=kubewatch-namespace 208 | INFO[0000] Processing add to namespace: kube-node-lease pkg=kubewatch-namespace 209 | INFO[0000] Processing add to namespace: kube-public pkg=kubewatch-namespace 210 | INFO[0000] Processing add to namespace: kube-system pkg=kubewatch-namespace 211 | INFO[0000] Processing add to namespace: default pkg=kubewatch-namespace 212 | .... 213 | ``` 214 | 215 | To Demonise Kubewatch container use 216 | 217 | ``` 218 | $ docker run --rm -d --network host -v $HOME/.kubewatch.yaml:/root/.kubewatch.yaml -v $HOME/.kube/config:/opt/bitnami/kubewatch/.kube/config --name kubewatch-app bitnami/kubewatch 219 | ``` 220 | 221 | # Configure 222 | 223 | Kubewatch supports `config` command for configuration. Config file will be saved at `$HOME/.kubewatch.yaml` 224 | 225 | ``` 226 | $ kubewatch config -h 227 | 228 | config command allows admin setup his own configuration for running kubewatch 229 | 230 | Usage: 231 | kubewatch config [flags] 232 | kubewatch config [command] 233 | 234 | Available Commands: 235 | add add webhook config to .kubewatch.yaml 236 | test test handler config present in .kubewatch.yaml 237 | view view .kubewatch.yaml 238 | 239 | Flags: 240 | -h, --help help for config 241 | 242 | Use "kubewatch config [command] --help" for more information about a command. 243 | ``` 244 | ### Example: 245 | 246 | ### slack: 247 | 248 | - Create a [slack Bot](https://my.slack.com/services/new/bot) 249 | 250 | - Edit the Bot to customize its name, icon and retrieve the API token (it starts with `xoxb-`). 251 | 252 | - Invite the Bot into your channel by typing: `/invite @name_of_your_bot` in the Slack message area. 253 | 254 | - Add Api token to kubewatch config using the following steps 255 | 256 | ```console 257 | $ kubewatch config add slack --channel --token 258 | ``` 259 | You have an altenative choice to set your SLACK token, channel via environment variables: 260 | 261 | ```console 262 | $ export KW_SLACK_TOKEN='XXXXXXXXXXXXXXXX' 263 | $ export KW_SLACK_CHANNEL='#channel_name' 264 | ``` 265 | 266 | ### flock: 267 | 268 | - Create a [flock bot](https://docs.flock.com/display/flockos/Bots). 269 | 270 | - Add flock webhook url to config using the following command. 271 | ```console 272 | $ kubewatch config add flock --url 273 | ``` 274 | You have an altenative choice to set your FLOCK URL 275 | 276 | ```console 277 | $ export KW_FLOCK_URL='https://api.flock.com/hooks/sendMessage/XXXXXXXX' 278 | ``` 279 | 280 | ## Testing Config 281 | 282 | To test the handler config by send test messages use the following command. 283 | ``` 284 | $ kubewatch config test -h 285 | 286 | Tests handler configs present in .kubewatch.yaml by sending test messages 287 | 288 | Usage: 289 | kubewatch config test [flags] 290 | 291 | Flags: 292 | -h, --help help for test 293 | ``` 294 | 295 | #### Example: 296 | 297 | ``` 298 | $ kubewatch config test 299 | 300 | Testing Handler configs from .kubewatch.yaml 301 | 2019/06/03 12:29:23 Message successfully sent to channel ABCD at 1559545162.000100 302 | ``` 303 | 304 | ## Viewing config 305 | To view the entire config file `$HOME/.kubewatch.yaml` use the following command. 306 | ``` 307 | $ kubewatch config view 308 | Contents of .kubewatch.yaml 309 | 310 | handler: 311 | slack: 312 | token: xoxb-xxxxx-yyyy-zzz 313 | channel: kube-watch 314 | hipchat: 315 | token: "" 316 | room: "" 317 | url: "" 318 | mattermost: 319 | channel: "" 320 | url: "" 321 | username: "" 322 | flock: 323 | url: "" 324 | webhook: 325 | url: "" 326 | resource: 327 | deployment: false 328 | replicationcontroller: false 329 | replicaset: false 330 | daemonset: false 331 | services: false 332 | pod: true 333 | job: false 334 | node: false 335 | clusterrole: false 336 | serviceaccount: false 337 | persistentvolume: false 338 | namespace: false 339 | secret: false 340 | configmap: false 341 | ingress: false 342 | namespace: "" 343 | 344 | ``` 345 | 346 | 347 | ## Resources 348 | 349 | To manage the resources being watched, use the following command, changes will be saved to `$HOME/.kubewatch.yaml`. 350 | 351 | ``` 352 | $ kubewatch resource -h 353 | 354 | manage resources to be watched 355 | 356 | Usage: 357 | kubewatch resource [flags] 358 | kubewatch resource [command] 359 | 360 | Available Commands: 361 | add adds specific resources to be watched 362 | remove remove specific resources being watched 363 | 364 | Flags: 365 | --clusterrole watch for cluster roles 366 | --cm watch for plain configmaps 367 | --deploy watch for deployments 368 | --ds watch for daemonsets 369 | -h, --help help for resource 370 | --ing watch for ingresses 371 | --job watch for jobs 372 | --node watch for Nodes 373 | --ns watch for namespaces 374 | --po watch for pods 375 | --pv watch for persistent volumes 376 | --rc watch for replication controllers 377 | --rs watch for replicasets 378 | --sa watch for service accounts 379 | --secret watch for plain secrets 380 | --svc watch for services 381 | 382 | Use "kubewatch resource [command] --help" for more information about a command. 383 | 384 | ``` 385 | 386 | ### Add/Remove resource: 387 | ``` 388 | $ kubewatch resource add -h 389 | 390 | adds specific resources to be watched 391 | 392 | Usage: 393 | kubewatch resource add [flags] 394 | 395 | Flags: 396 | -h, --help help for add 397 | 398 | Global Flags: 399 | --clusterrole watch for cluster roles 400 | --cm watch for plain configmaps 401 | --deploy watch for deployments 402 | --ds watch for daemonsets 403 | --ing watch for ingresses 404 | --job watch for jobs 405 | --node watch for Nodes 406 | --ns watch for namespaces 407 | --po watch for pods 408 | --pv watch for persistent volumes 409 | --rc watch for replication controllers 410 | --rs watch for replicasets 411 | --sa watch for service accounts 412 | --secret watch for plain secrets 413 | --svc watch for services 414 | 415 | ``` 416 | 417 | ### Example: 418 | 419 | ```console 420 | # rc, po and svc will be watched 421 | $ kubewatch resource add --rc --po --svc 422 | 423 | # rc, po and svc will be stopped from being watched 424 | $ kubewatch resource remove --rc --po --svc 425 | ``` 426 | 427 | # Build 428 | 429 | ### Using go 430 | 431 | Clone the repository anywhere: 432 | ```console 433 | $ git clone https://github.com/bitnami-labs/kubewatch.git 434 | $ cd kubewatch 435 | $ go build 436 | ``` 437 | or 438 | 439 | You can also use the Makefile directly: 440 | 441 | ```console 442 | $ make build 443 | ``` 444 | 445 | #### Prerequisites 446 | 447 | - You need to have [Go](http://golang.org) (v1.5 or later) installed. Make sure to set `$GOPATH` 448 | 449 | 450 | ### Using Docker 451 | 452 | ```console 453 | $ make docker-image 454 | $ docker images 455 | REPOSITORY TAG IMAGE ID CREATED SIZE 456 | kubewatch latest 919896d3cd90 3 minutes ago 27.9MB 457 | ``` 458 | #### Prerequisites 459 | 460 | - you need to have [docker](https://docs.docker.com/) installed. 461 | 462 | # Contribution 463 | 464 | Refer to the [contribution guidelines](docs/CONTRIBUTION.md) to get started. 465 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | "continuous-integration/travis-ci/push" 3 | ] 4 | 5 | pr_status = [ 6 | "continuous-integration/travis-ci/pr" 7 | ] 8 | 9 | required_approvals = 1 10 | delete_merged_branches = true 11 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | "io/ioutil" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/bitnami-labs/kubewatch/config" 26 | "github.com/bitnami-labs/kubewatch/pkg/client" 27 | "github.com/bitnami-labs/kubewatch/pkg/event" 28 | "github.com/sirupsen/logrus" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | const kubewatchConfigFile = ".kubewatch.yaml" 33 | 34 | // configCmd represents the config command 35 | var configCmd = &cobra.Command{ 36 | Use: "config", 37 | Short: "modify kubewatch configuration", 38 | Long: ` 39 | config command allows configuration of ~/.kubewatch.yaml for running kubewatch`, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | cmd.Help() 42 | }, 43 | } 44 | 45 | var configAddCmd = &cobra.Command{ 46 | Use: "add", 47 | Short: "add webhook config to ~/.kubewatch.yaml", 48 | Long: ` 49 | Adds webhook config to ~/.kubewatch.yaml`, 50 | Run: func(cmd *cobra.Command, args []string) { 51 | cmd.Help() 52 | }, 53 | } 54 | 55 | var configTestCmd = &cobra.Command{ 56 | Use: "test", 57 | Short: "test handler config present in ~/.kubewatch.yaml", 58 | Long: ` 59 | Tests handler configs present in ~/.kubewatch.yaml by sending test messages`, 60 | Run: func(cmd *cobra.Command, args []string) { 61 | fmt.Println("Testing Handler configs from .kubewatch.yaml") 62 | conf, err := config.New() 63 | if err != nil { 64 | logrus.Fatal(err) 65 | } 66 | eventHandler := client.ParseEventHandler(conf) 67 | e := event.Event{ 68 | Namespace: "testNamespace", 69 | Name: "testResource", 70 | Kind: "testKind", 71 | Component: "testComponent", 72 | Host: "testHost", 73 | Reason: "Tested", 74 | Status: "Normal", 75 | } 76 | eventHandler.Handle(e) 77 | }, 78 | } 79 | 80 | var configSampleCmd = &cobra.Command{ 81 | Use: "sample", 82 | Short: "Show a sample config file", 83 | Long: ` 84 | Print a sample config file which can be put in ~/.kubewatch.yaml`, 85 | Run: func(cmd *cobra.Command, args []string) { 86 | fmt.Print(config.ConfigSample) 87 | }, 88 | } 89 | 90 | var configViewCmd = &cobra.Command{ 91 | Use: "view", 92 | Short: "view ~/.kubewatch.yaml", 93 | Long: ` 94 | Display the contents of the contents of ~/.kubewatch.yaml`, 95 | Run: func(cmd *cobra.Command, args []string) { 96 | fmt.Fprintln(os.Stderr, "Contents of ~/.kubewatch.yaml") 97 | configFile, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), kubewatchConfigFile)) 98 | if err != nil { 99 | fmt.Fprintf(os.Stderr, "%v\n", err) 100 | os.Exit(1) 101 | } 102 | fmt.Print(string(configFile)) 103 | }, 104 | } 105 | 106 | func init() { 107 | RootCmd.AddCommand(configCmd) 108 | configCmd.AddCommand( 109 | configAddCmd, 110 | configTestCmd, 111 | configSampleCmd, 112 | configViewCmd, 113 | ) 114 | 115 | configAddCmd.AddCommand( 116 | slackConfigCmd, 117 | hipchatConfigCmd, 118 | mattermostConfigCmd, 119 | flockConfigCmd, 120 | webhookConfigCmd, 121 | msteamsConfigCmd, 122 | smtpConfigCmd, 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /cmd/flock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // flockConfigCmd represents the flock subcommand 26 | var flockConfigCmd = &cobra.Command{ 27 | Use: "flock", 28 | Short: "specific flock configuration", 29 | Long: `specific flock configuration`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | conf, err := config.New() 32 | if err != nil { 33 | logrus.Fatal(err) 34 | } 35 | 36 | url, err := cmd.Flags().GetString("url") 37 | if err == nil { 38 | if len(url) > 0 { 39 | conf.Handler.Flock.Url = url 40 | } 41 | } else { 42 | logrus.Fatal(err) 43 | } 44 | 45 | if err = conf.Write(); err != nil { 46 | logrus.Fatal(err) 47 | } 48 | }, 49 | } 50 | 51 | func init() { 52 | flockConfigCmd.Flags().StringP("url", "u", "", "Specify Flock url") 53 | } 54 | -------------------------------------------------------------------------------- /cmd/hipchat.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // hipchatConfigCmd represents the hipchat subcommand 26 | var hipchatConfigCmd = &cobra.Command{ 27 | Use: "hipchat", 28 | Short: "specific hipchat configuration", 29 | Long: `specific hipchat configuration`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | conf, err := config.New() 32 | if err != nil { 33 | logrus.Fatal(err) 34 | } 35 | 36 | token, err := cmd.Flags().GetString("token") 37 | if err == nil { 38 | if len(token) > 0 { 39 | conf.Handler.Hipchat.Token = token 40 | } 41 | } else { 42 | logrus.Fatal(err) 43 | } 44 | room, err := cmd.Flags().GetString("room") 45 | if err == nil { 46 | if len(room) > 0 { 47 | conf.Handler.Hipchat.Room = room 48 | } 49 | } else { 50 | logrus.Fatal(err) 51 | } 52 | 53 | if err = conf.Write(); err != nil { 54 | logrus.Fatal(err) 55 | } 56 | }, 57 | } 58 | 59 | func init() { 60 | hipchatConfigCmd.Flags().StringP("room", "r", "", "Specify hipchat room") 61 | hipchatConfigCmd.Flags().StringP("token", "t", "", "Specify hipchat token") 62 | hipchatConfigCmd.Flags().StringP("url", "u", "", "Specify hipchat server url") 63 | } 64 | -------------------------------------------------------------------------------- /cmd/mattermost.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // mattermostConfigCmd represents the mattermost subcommand 26 | var mattermostConfigCmd = &cobra.Command{ 27 | Use: "mattermost", 28 | Short: "specific mattermost configuration", 29 | Long: `specific mattermost configuration`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | conf, err := config.New() 32 | if err != nil { 33 | logrus.Fatal(err) 34 | } 35 | 36 | channel, err := cmd.Flags().GetString("channel") 37 | if err == nil { 38 | if len(channel) > 0 { 39 | conf.Handler.Mattermost.Channel = channel 40 | } 41 | } else { 42 | logrus.Fatal(err) 43 | } 44 | 45 | url, err := cmd.Flags().GetString("url") 46 | if err == nil { 47 | if len(url) > 0 { 48 | conf.Handler.Mattermost.Url = url 49 | } 50 | } else { 51 | logrus.Fatal(err) 52 | } 53 | 54 | username, err := cmd.Flags().GetString("username") 55 | if err == nil { 56 | if len(url) > 0 { 57 | conf.Handler.Mattermost.Username = username 58 | } 59 | } else { 60 | logrus.Fatal(err) 61 | } 62 | 63 | if err = conf.Write(); err != nil { 64 | logrus.Fatal(err) 65 | } 66 | }, 67 | } 68 | 69 | func init() { 70 | mattermostConfigCmd.Flags().StringP("channel", "c", "", "Specify Mattermost channel") 71 | mattermostConfigCmd.Flags().StringP("url", "u", "", "Specify Mattermost url") 72 | mattermostConfigCmd.Flags().StringP("username", "n", "", "Specify Mattermost username") 73 | } 74 | -------------------------------------------------------------------------------- /cmd/msteams.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // msteamsConfigCmd represents the msteams subcommand 26 | var msteamsConfigCmd = &cobra.Command{ 27 | Use: "MS Teams FLAG", 28 | Short: "specific MS Teams configuration", 29 | Long: `specific MS Teams configuration`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | conf, err := config.New() 32 | if err != nil { 33 | logrus.Fatal(err) 34 | } 35 | 36 | webhookURL, err := cmd.Flags().GetString("webhookurl") 37 | if err == nil { 38 | if len(webhookURL) > 0 { 39 | conf.Handler.MSTeams.WebhookURL = webhookURL 40 | } 41 | } else { 42 | logrus.Fatal(err) 43 | } 44 | 45 | if err = conf.Write(); err != nil { 46 | logrus.Fatal(err) 47 | } 48 | }, 49 | } 50 | 51 | func init() { 52 | msteamsConfigCmd.Flags().StringP("webhookurl", "w", "", "Specify MS Teams webhook URL") 53 | } 54 | -------------------------------------------------------------------------------- /cmd/resource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // resourceConfigCmd represents the resource subcommand 26 | var resourceConfigCmd = &cobra.Command{ 27 | Use: "resource", 28 | Short: "manage resources to be watched", 29 | Long: ` 30 | manage resources to be watched`, 31 | Run: func(cmd *cobra.Command, args []string) { 32 | 33 | // warn for too few arguments 34 | if len(args) < 2 { 35 | logrus.Warn("Too few arguments to Command \"resource\".\nMinimum 2 arguments required: subcommand, resource flags") 36 | } 37 | // display help 38 | cmd.Help() 39 | }, 40 | } 41 | 42 | // resourceConfigAddCmd represents the resource add subcommand 43 | var resourceConfigAddCmd = &cobra.Command{ 44 | Use: "add", 45 | Short: "adds specific resources to be watched", 46 | Long: ` 47 | adds specific resources to be watched`, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | conf, err := config.New() 50 | if err != nil { 51 | logrus.Fatal(err) 52 | } 53 | 54 | // add resource to config 55 | configureResource("add", cmd, conf) 56 | }, 57 | } 58 | 59 | // resourceConfigRemoveCmd represents the resource remove subcommand 60 | var resourceConfigRemoveCmd = &cobra.Command{ 61 | Use: "remove", 62 | Short: "remove specific resources being watched", 63 | Long: ` 64 | remove specific resources being watched`, 65 | Run: func(cmd *cobra.Command, args []string) { 66 | conf, err := config.New() 67 | if err != nil { 68 | logrus.Fatal(err) 69 | } 70 | 71 | // remove resource from config 72 | configureResource("remove", cmd, conf) 73 | }, 74 | } 75 | 76 | // configures resource in config based on operation add/remove 77 | func configureResource(operation string, cmd *cobra.Command, conf *config.Config) { 78 | 79 | // flags struct 80 | flags := []struct { 81 | resourceStr string 82 | resourceToWatch *bool 83 | }{ 84 | { 85 | "svc", 86 | &conf.Resource.Services, 87 | }, 88 | { 89 | "deploy", 90 | &conf.Resource.Deployment, 91 | }, 92 | { 93 | "po", 94 | &conf.Resource.Pod, 95 | }, 96 | { 97 | "rs", 98 | &conf.Resource.ReplicaSet, 99 | }, 100 | { 101 | "rc", 102 | &conf.Resource.ReplicationController, 103 | }, 104 | { 105 | "ns", 106 | &conf.Resource.Namespace, 107 | }, 108 | { 109 | "job", 110 | &conf.Resource.Job, 111 | }, 112 | { 113 | "pv", 114 | &conf.Resource.PersistentVolume, 115 | }, 116 | { 117 | "ds", 118 | &conf.Resource.DaemonSet, 119 | }, 120 | { 121 | "secret", 122 | &conf.Resource.Secret, 123 | }, 124 | { 125 | "cm", 126 | &conf.Resource.ConfigMap, 127 | }, 128 | { 129 | "ing", 130 | &conf.Resource.Ingress, 131 | }, 132 | { 133 | "node", 134 | &conf.Resource.Node, 135 | }, 136 | { 137 | "clusterrole", 138 | &conf.Resource.ClusterRole, 139 | }, 140 | { 141 | "sa", 142 | &conf.Resource.ServiceAccount, 143 | }, 144 | } 145 | 146 | for _, flag := range flags { 147 | b, err := cmd.Flags().GetBool(flag.resourceStr) 148 | if err == nil { 149 | if b { 150 | switch operation { 151 | case "add": 152 | *flag.resourceToWatch = true 153 | logrus.Infof("resource %s configured", flag.resourceStr) 154 | case "remove": 155 | *flag.resourceToWatch = false 156 | logrus.Infof("resource %s removed", flag.resourceStr) 157 | } 158 | } 159 | } else { 160 | logrus.Fatal(flag.resourceStr, err) 161 | } 162 | } 163 | 164 | if err := conf.Write(); err != nil { 165 | logrus.Fatal(err) 166 | } 167 | } 168 | 169 | func init() { 170 | RootCmd.AddCommand(resourceConfigCmd) 171 | resourceConfigCmd.AddCommand( 172 | resourceConfigAddCmd, 173 | resourceConfigRemoveCmd, 174 | ) 175 | // Add resource object flags as PersistentFlags to resourceConfigCmd 176 | resourceConfigCmd.PersistentFlags().Bool("svc", false, "watch for services") 177 | resourceConfigCmd.PersistentFlags().Bool("deploy", false, "watch for deployments") 178 | resourceConfigCmd.PersistentFlags().Bool("po", false, "watch for pods") 179 | resourceConfigCmd.PersistentFlags().Bool("rc", false, "watch for replication controllers") 180 | resourceConfigCmd.PersistentFlags().Bool("rs", false, "watch for replicasets") 181 | resourceConfigCmd.PersistentFlags().Bool("ns", false, "watch for namespaces") 182 | resourceConfigCmd.PersistentFlags().Bool("pv", false, "watch for persistent volumes") 183 | resourceConfigCmd.PersistentFlags().Bool("job", false, "watch for jobs") 184 | resourceConfigCmd.PersistentFlags().Bool("ds", false, "watch for daemonsets") 185 | resourceConfigCmd.PersistentFlags().Bool("secret", false, "watch for plain secrets") 186 | resourceConfigCmd.PersistentFlags().Bool("cm", false, "watch for plain configmaps") 187 | resourceConfigCmd.PersistentFlags().Bool("ing", false, "watch for ingresses") 188 | resourceConfigCmd.PersistentFlags().Bool("node", false, "watch for Nodes") 189 | resourceConfigCmd.PersistentFlags().Bool("clusterrole", false, "watch for cluster roles") 190 | resourceConfigCmd.PersistentFlags().Bool("sa", false, "watch for service accounts") 191 | } 192 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/bitnami-labs/kubewatch/config" 24 | c "github.com/bitnami-labs/kubewatch/pkg/client" 25 | "github.com/sirupsen/logrus" 26 | "github.com/spf13/cobra" 27 | "github.com/spf13/viper" 28 | ) 29 | 30 | var cfgFile string 31 | 32 | // RootCmd represents the base command when called without any subcommands 33 | var RootCmd = &cobra.Command{ 34 | Use: "kubewatch", 35 | Short: "A watcher for Kubernetes", 36 | Long: ` 37 | Kubewatch: A watcher for Kubernetes 38 | 39 | kubewatch is a Kubernetes watcher that could publishes notification 40 | to Slack/hipchat/mattermost/flock channels. It watches the cluster 41 | for resource changes and notifies them through webhooks. 42 | 43 | supported webhooks: 44 | - slack 45 | - hipchat 46 | - mattermost 47 | - flock 48 | - webhook 49 | `, 50 | 51 | Run: func(cmd *cobra.Command, args []string) { 52 | config := &config.Config{} 53 | if err := config.Load(); err != nil { 54 | logrus.Fatal(err) 55 | } 56 | config.CheckMissingResourceEnvvars() 57 | c.Run(config) 58 | }, 59 | } 60 | 61 | // Execute adds all child commands to the root command sets flags appropriately. 62 | // This is called by main.main(). It only needs to happen once to the rootCmd. 63 | func Execute() { 64 | if err := RootCmd.Execute(); err != nil { 65 | fmt.Println(err) 66 | os.Exit(-1) 67 | } 68 | } 69 | 70 | func init() { 71 | cobra.OnInitialize(initConfig) 72 | 73 | // Disable Help subcommand 74 | RootCmd.SetHelpCommand(&cobra.Command{ 75 | Use: "no-help", 76 | Hidden: true, 77 | }) 78 | //RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.kubewatch.yaml)") 79 | } 80 | 81 | // initConfig reads in config file and ENV variables if set. 82 | func initConfig() { 83 | if cfgFile != "" { // enable ability to specify config file via flag 84 | viper.SetConfigFile(cfgFile) 85 | } 86 | 87 | viper.SetConfigName(kubewatchConfigFile) // name of config file (without extension) 88 | viper.AddConfigPath("$HOME") // adding home directory as first search path 89 | viper.AutomaticEnv() // read in environment variables that match 90 | 91 | // If a config file is found, read it in. 92 | if err := viper.ReadInConfig(); err == nil { 93 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cmd/slack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // slackConfigCmd represents the slack subcommand 26 | var slackConfigCmd = &cobra.Command{ 27 | Use: "slack", 28 | Short: "specific slack configuration", 29 | Long: `specific slack configuration`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | conf, err := config.New() 32 | if err != nil { 33 | logrus.Fatal(err) 34 | } 35 | 36 | token, err := cmd.Flags().GetString("token") 37 | if err == nil { 38 | if len(token) > 0 { 39 | conf.Handler.Slack.Token = token 40 | } 41 | } else { 42 | logrus.Fatal(err) 43 | } 44 | channel, err := cmd.Flags().GetString("channel") 45 | if err == nil { 46 | if len(channel) > 0 { 47 | conf.Handler.Slack.Channel = channel 48 | } 49 | } else { 50 | logrus.Fatal(err) 51 | } 52 | title, err := cmd.Flags().GetString("title") 53 | if err == nil { 54 | if len(title) > 0 { 55 | conf.Handler.Slack.Title = title 56 | } 57 | } 58 | 59 | if err = conf.Write(); err != nil { 60 | logrus.Fatal(err) 61 | } 62 | }, 63 | } 64 | 65 | func init() { 66 | slackConfigCmd.Flags().StringP("channel", "c", "", "Specify slack channel") 67 | slackConfigCmd.Flags().StringP("token", "t", "", "Specify slack token") 68 | slackConfigCmd.Flags().StringP("title", "", "", "Specify slack msg title") 69 | } 70 | -------------------------------------------------------------------------------- /cmd/smtp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 VMware 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/bitnami-labs/kubewatch/pkg/handlers/smtp" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | // smtpConfigCmd represents the smtp subcommand 28 | var smtpConfigCmd = &cobra.Command{ 29 | Use: "smtp", 30 | Short: "specific smtp configuration", 31 | Long: `specific smtp configuration`, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | fmt.Fprintf(os.Stderr, "CLI setters not implemented yet, please edit ~/.kubewatch.yaml directly. Example:\n\n%s", smtp.ConfigExample) 34 | }, 35 | } 36 | 37 | func init() { 38 | } 39 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/sirupsen/logrus" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | var ( 25 | buildDate, gitCommit string 26 | ) 27 | 28 | var versionCmd = &cobra.Command{ 29 | Use: "version", 30 | Short: "print version", 31 | Long: `print version`, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | versionPrettyString() 34 | }, 35 | } 36 | 37 | func versionPrettyString() { 38 | logrus.Info("gitCommit: ", gitCommit) 39 | logrus.Info("buildDate: ", buildDate) 40 | } 41 | 42 | func init() { 43 | RootCmd.AddCommand(versionCmd) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Bitnami 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // webhookConfigCmd represents the webhook subcommand 26 | var webhookConfigCmd = &cobra.Command{ 27 | Use: "webhook", 28 | Short: "specific webhook configuration", 29 | Long: `specific webhook configuration`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | conf, err := config.New() 32 | if err != nil { 33 | logrus.Fatal(err) 34 | } 35 | 36 | url, err := cmd.Flags().GetString("url") 37 | if err == nil { 38 | if len(url) > 0 { 39 | conf.Handler.Webhook.Url = url 40 | } 41 | } else { 42 | logrus.Fatal(err) 43 | } 44 | 45 | if err = conf.Write(); err != nil { 46 | logrus.Fatal(err) 47 | } 48 | }, 49 | } 50 | 51 | func init() { 52 | webhookConfigCmd.Flags().StringP("url", "u", "", "Specify Webhook url") 53 | } 54 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | //go:generate bash -c "go install ../tools/yannotated && yannotated -o sample.go -format go -package config -type Config" 18 | 19 | package config 20 | 21 | import ( 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "runtime" 26 | 27 | "gopkg.in/yaml.v3" 28 | ) 29 | 30 | var ( 31 | // ConfigFileName stores file of config 32 | ConfigFileName = ".kubewatch.yaml" 33 | 34 | // ConfigSample is a sample configuration file. 35 | ConfigSample = yannotated 36 | ) 37 | 38 | // Handler contains handler configuration 39 | type Handler struct { 40 | Slack Slack `json:"slack"` 41 | Hipchat Hipchat `json:"hipchat"` 42 | Mattermost Mattermost `json:"mattermost"` 43 | Flock Flock `json:"flock"` 44 | Webhook Webhook `json:"webhook"` 45 | MSTeams MSTeams `json:"msteams"` 46 | SMTP SMTP `json:"smtp"` 47 | } 48 | 49 | // Resource contains resource configuration 50 | type Resource struct { 51 | Deployment bool `json:"deployment"` 52 | ReplicationController bool `json:"rc"` 53 | ReplicaSet bool `json:"rs"` 54 | DaemonSet bool `json:"ds"` 55 | Services bool `json:"svc"` 56 | Pod bool `json:"po"` 57 | Job bool `json:"job"` 58 | Node bool `json:"node"` 59 | ClusterRole bool `json:"clusterrole"` 60 | ServiceAccount bool `json:"sa"` 61 | PersistentVolume bool `json:"pv"` 62 | Namespace bool `json:"ns"` 63 | Secret bool `json:"secret"` 64 | ConfigMap bool `json:"configmap"` 65 | Ingress bool `json:"ing"` 66 | } 67 | 68 | // Config struct contains kubewatch configuration 69 | type Config struct { 70 | // Handlers know how to send notifications to specific services. 71 | Handler Handler `json:"handler"` 72 | 73 | //Reason []string `json:"reason"` 74 | 75 | // Resources to watch. 76 | Resource Resource `json:"resource"` 77 | 78 | // For watching specific namespace, leave it empty for watching all. 79 | // this config is ignored when watching namespaces 80 | Namespace string `json:"namespace,omitempty"` 81 | } 82 | 83 | // Slack contains slack configuration 84 | type Slack struct { 85 | // Slack "legacy" API token. 86 | Token string `json:"token"` 87 | // Slack channel. 88 | Channel string `json:"channel"` 89 | // Title of the message. 90 | Title string `json:"title"` 91 | } 92 | 93 | // Hipchat contains hipchat configuration 94 | type Hipchat struct { 95 | // Hipchat token. 96 | Token string `json:"token"` 97 | // Room name. 98 | Room string `json:"room"` 99 | // URL of the hipchat server. 100 | Url string `json:"url"` 101 | } 102 | 103 | // Mattermost contains mattermost configuration 104 | type Mattermost struct { 105 | Channel string `json:"room"` 106 | Url string `json:"url"` 107 | Username string `json:"username"` 108 | } 109 | 110 | // Flock contains flock configuration 111 | type Flock struct { 112 | // URL of the flock API. 113 | Url string `json:"url"` 114 | } 115 | 116 | // Webhook contains webhook configuration 117 | type Webhook struct { 118 | // Webhook URL. 119 | Url string `json:"url"` 120 | } 121 | 122 | // MSTeams contains MSTeams configuration 123 | type MSTeams struct { 124 | // MSTeams API Webhook URL. 125 | WebhookURL string `json:"webhookurl"` 126 | } 127 | 128 | // SMTP contains SMTP configuration. 129 | type SMTP struct { 130 | // Destination e-mail address. 131 | To string `json:"to" yaml:"to,omitempty"` 132 | // Sender e-mail address . 133 | From string `json:"from" yaml:"from,omitempty"` 134 | // Smarthost, aka "SMTP server"; address of server used to send email. 135 | Smarthost string `json:"smarthost" yaml:"smarthost,omitempty"` 136 | // Subject of the outgoing emails. 137 | Subject string `json:"subject" yaml:"subject,omitempty"` 138 | // Extra e-mail headers to be added to all outgoing messages. 139 | Headers map[string]string `json:"headers" yaml:"headers,omitempty"` 140 | // Authentication parameters. 141 | Auth SMTPAuth `json:"auth" yaml:"auth,omitempty"` 142 | // If "true" forces secure SMTP protocol (AKA StartTLS). 143 | RequireTLS bool `json:"requireTLS" yaml:"requireTLS"` 144 | // SMTP hello field (optional) 145 | Hello string `json:"hello" yaml:"hello,omitempty"` 146 | } 147 | 148 | type SMTPAuth struct { 149 | // Username for PLAN and LOGIN auth mechanisms. 150 | Username string `json:"username" yaml:"username,omitempty"` 151 | // Password for PLAIN and LOGIN auth mechanisms. 152 | Password string `json:"password" yaml:"password,omitempty"` 153 | // Identity for PLAIN auth mechanism 154 | Identity string `json:"identity" yaml:"identity,omitempty"` 155 | // Secret for CRAM-MD5 auth mechanism 156 | Secret string `json:"secret" yaml:"secret,omitempty"` 157 | } 158 | 159 | // New creates new config object 160 | func New() (*Config, error) { 161 | c := &Config{} 162 | if err := c.Load(); err != nil { 163 | return c, err 164 | } 165 | 166 | return c, nil 167 | } 168 | 169 | func createIfNotExist() error { 170 | // create file if not exist 171 | configFile := filepath.Join(configDir(), ConfigFileName) 172 | _, err := os.Stat(configFile) 173 | if err != nil { 174 | if os.IsNotExist(err) { 175 | file, err := os.Create(configFile) 176 | if err != nil { 177 | return err 178 | } 179 | file.Close() 180 | } else { 181 | return err 182 | } 183 | } 184 | return nil 185 | } 186 | 187 | // Load loads configuration from config file 188 | func (c *Config) Load() error { 189 | err := createIfNotExist() 190 | if err != nil { 191 | return err 192 | } 193 | 194 | file, err := os.Open(getConfigFile()) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | b, err := ioutil.ReadAll(file) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | if len(b) != 0 { 205 | return yaml.Unmarshal(b, c) 206 | } 207 | 208 | return nil 209 | } 210 | 211 | // CheckMissingResourceEnvvars will read the environment for equivalent config variables to set 212 | func (c *Config) CheckMissingResourceEnvvars() { 213 | if !c.Resource.DaemonSet && os.Getenv("KW_DAEMONSET") == "true" { 214 | c.Resource.DaemonSet = true 215 | } 216 | if !c.Resource.ReplicaSet && os.Getenv("KW_REPLICASET") == "true" { 217 | c.Resource.ReplicaSet = true 218 | } 219 | if !c.Resource.Namespace && os.Getenv("KW_NAMESPACE") == "true" { 220 | c.Resource.Namespace = true 221 | } 222 | if !c.Resource.Deployment && os.Getenv("KW_DEPLOYMENT") == "true" { 223 | c.Resource.Deployment = true 224 | } 225 | if !c.Resource.Pod && os.Getenv("KW_POD") == "true" { 226 | c.Resource.Pod = true 227 | } 228 | if !c.Resource.ReplicationController && os.Getenv("KW_REPLICATION_CONTROLLER") == "true" { 229 | c.Resource.ReplicationController = true 230 | } 231 | if !c.Resource.Services && os.Getenv("KW_SERVICE") == "true" { 232 | c.Resource.Services = true 233 | } 234 | if !c.Resource.Job && os.Getenv("KW_JOB") == "true" { 235 | c.Resource.Job = true 236 | } 237 | if !c.Resource.PersistentVolume && os.Getenv("KW_PERSISTENT_VOLUME") == "true" { 238 | c.Resource.PersistentVolume = true 239 | } 240 | if !c.Resource.Secret && os.Getenv("KW_SECRET") == "true" { 241 | c.Resource.Secret = true 242 | } 243 | if !c.Resource.ConfigMap && os.Getenv("KW_CONFIGMAP") == "true" { 244 | c.Resource.ConfigMap = true 245 | } 246 | if !c.Resource.Ingress && os.Getenv("KW_INGRESS") == "true" { 247 | c.Resource.Ingress = true 248 | } 249 | if !c.Resource.Node && os.Getenv("KW_NODE") == "true" { 250 | c.Resource.Node = true 251 | } 252 | if !c.Resource.ServiceAccount && os.Getenv("KW_SERVICE_ACCOUNT") == "true" { 253 | c.Resource.ServiceAccount = true 254 | } 255 | if !c.Resource.ClusterRole && os.Getenv("KW_CLUSTER_ROLE") == "true" { 256 | c.Resource.ClusterRole = true 257 | } 258 | if (c.Handler.Slack.Channel == "") && (os.Getenv("SLACK_CHANNEL") != "") { 259 | c.Handler.Slack.Channel = os.Getenv("SLACK_CHANNEL") 260 | } 261 | if (c.Handler.Slack.Token == "") && (os.Getenv("SLACK_TOKEN") != "") { 262 | c.Handler.Slack.Token = os.Getenv("SLACK_TOKEN") 263 | } 264 | } 265 | 266 | func (c *Config) Write() error { 267 | f, err := os.OpenFile(getConfigFile(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 268 | if err != nil { 269 | return err 270 | } 271 | defer f.Close() 272 | 273 | enc := yaml.NewEncoder(f) 274 | enc.SetIndent(2) // compat with old versions of kubewatch 275 | return enc.Encode(c) 276 | } 277 | 278 | func getConfigFile() string { 279 | configFile := filepath.Join(configDir(), ConfigFileName) 280 | if _, err := os.Stat(configFile); err == nil { 281 | return configFile 282 | } 283 | 284 | return "" 285 | } 286 | 287 | func configDir() string { 288 | if configDir := os.Getenv("KW_CONFIG"); configDir != "" { 289 | return configDir 290 | } 291 | 292 | if runtime.GOOS == "windows" { 293 | home := os.Getenv("USERPROFILE") 294 | return home 295 | } 296 | return os.Getenv("HOME") 297 | //path := "/etc/kubewatch" 298 | //if _, err := os.Stat(path); os.IsNotExist(err) { 299 | // os.Mkdir(path, 755) 300 | //} 301 | //return path 302 | } 303 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | //"io/ioutil" 21 | //"os" 22 | //"testing" 23 | ) 24 | 25 | var configStr = ` 26 | { 27 | "handler": { 28 | "slack": { 29 | "channel": "slack_channel", 30 | "token": "slack_token" 31 | }, 32 | "webhook": { 33 | "url": "http://localhost:8080" 34 | } 35 | }, 36 | "reason": ["created", "deleted", "updated"], 37 | "resource": { 38 | "deployment": "false", 39 | "replicationcontroller": "false", 40 | "replicaset": "false", 41 | "daemonset": "false", 42 | "services": "false", 43 | "pod": "false", 44 | "secret": "true", 45 | "configmap": "true", 46 | "ingress": "false", 47 | }, 48 | } 49 | ` 50 | 51 | //func TestLoadOK(t *testing.T) { 52 | // content := []byte(configStr) 53 | // tmpConfigFile, err := ioutil.TempFile(homeDir(), "kubewatch") 54 | // if err != nil { 55 | // t.Fatalf("TestLoad(): %+v", err) 56 | // } 57 | // 58 | // defer func() { 59 | // _ = os.Remove(tmpConfigFile.Name()) 60 | // }() 61 | // 62 | // if _, err := tmpConfigFile.Write(content); err != nil { 63 | // t.Fatalf("TestLoad(): %+v", err) 64 | // } 65 | // if err := tmpConfigFile.Close(); err != nil { 66 | // t.Fatalf("TestLoad(): %+v", err) 67 | // } 68 | // 69 | // ConfigFileName = "kubewatch" 70 | // 71 | // c, err := New() 72 | // 73 | // err = c.Load() 74 | // if err != nil { 75 | // t.Fatalf("TestLoad(): %+v", err) 76 | // } 77 | //} 78 | -------------------------------------------------------------------------------- /config/sample.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var yannotated = `# Handlers know how to send notifications to specific services. 4 | handler: 5 | slack: 6 | # Slack "legacy" API token. 7 | token: "" 8 | # Slack channel. 9 | channel: "" 10 | # Title of the message. 11 | title: "" 12 | hipchat: 13 | # Hipchat token. 14 | token: "" 15 | # Room name. 16 | room: "" 17 | # URL of the hipchat server. 18 | url: "" 19 | mattermost: 20 | room: "" 21 | url: "" 22 | username: "" 23 | flock: 24 | # URL of the flock API. 25 | url: "" 26 | webhook: 27 | # Webhook URL. 28 | url: "" 29 | msteams: 30 | # MSTeams API Webhook URL. 31 | webhookurl: "" 32 | smtp: 33 | # Destination e-mail address. 34 | to: "" 35 | # Sender e-mail address . 36 | from: "" 37 | # Smarthost, aka "SMTP server"; address of server used to send email. 38 | smarthost: "" 39 | # Subject of the outgoing emails. 40 | subject: "" 41 | # Extra e-mail headers to be added to all outgoing messages. 42 | headers: {} 43 | # Authentication parameters. 44 | auth: 45 | # Username for PLAN and LOGIN auth mechanisms. 46 | username: "" 47 | # Password for PLAIN and LOGIN auth mechanisms. 48 | password: "" 49 | # Identity for PLAIN auth mechanism 50 | identity: "" 51 | # Secret for CRAM-MD5 auth mechanism 52 | secret: "" 53 | # If "true" forces secure SMTP protocol (AKA StartTLS). 54 | requireTLS: false 55 | # SMTP hello field (optional) 56 | hello: "" 57 | # Resources to watch. 58 | resource: 59 | deployment: false 60 | rc: false 61 | rs: false 62 | ds: false 63 | svc: false 64 | po: false 65 | job: false 66 | node: false 67 | clusterrole: false 68 | sa: false 69 | pv: false 70 | ns: false 71 | secret: false 72 | configmap: false 73 | ing: false 74 | # For watching specific namespace, leave it empty for watching all. 75 | # this config is ignored when watching namespaces 76 | namespace: "" 77 | ` 78 | -------------------------------------------------------------------------------- /docs/CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Contributing to Kubewatch 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## We use Github to develop kubewatch 11 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We use [Github Flow](https://guides.github.com/introduction/flow/index.html), so all code changes happen through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the test suite passes. 20 | 5. Make sure your code lints. (by using gofmt tool `gofmt -s -w . `) 21 | 6. Issue that pull request! 22 | 23 | ## Report bugs using Github's [issues](https://github.com/bitnami-labs/kubewatch/issues) 24 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! 25 | 26 | ## Write bug reports with detail, background, and sample code 27 | **Great Bug Reports** tend to have: 28 | 29 | - A quick summary and/or background 30 | - Steps to reproduce 31 | - Be specific! 32 | - Give sample code if you can. 33 | - What you expected would happen 34 | - What actually happens 35 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 36 | 37 | ## Use a Consistent Coding Style 38 | 39 | Golang Coding Style can be followed 40 | * You can try running `gofmt -s -w .` for style unification 41 | 42 | ## License 43 | By contributing, you agree that your contributions will be licensed under its Apache License 2.0. 44 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Kubewatch 2 | 3 | Kubewatch contains three components: controller, config, handler 4 | 5 | ![Kubewatch Diagram](kubewatch.png?raw=true "Kubewatch Overview") 6 | 7 | ## Config 8 | 9 | The config object contains `kubewatch` configuration, like handlers, filters. 10 | 11 | A config object is used to creating new client. 12 | 13 | ## Controller 14 | 15 | The controller initializes using the config object by reading the `.kubewatch.yaml` or command line arguments. 16 | If the parameters are not fully mentioned, the config falls back to read a set of standard environment variables. 17 | 18 | Controller creates necessary `SharedIndexInformer`s provided by `kubernetes/client-go` for listening and watching 19 | resource changes. Controller updates this subscription information with Kubernetes API Server. 20 | 21 | Whenever, the Kubernetes Controller Manager gets events related to the subscribed resources, it pushes the events to 22 | `SharedIndexInformer`. This in-turn puts the events onto a rate-limiting queue for better handling of the events. 23 | 24 | Controller picks the events from the queue and hands over the events to the appropriate handler after 25 | necessary filtering. 26 | 27 | ## Handler 28 | 29 | Handler manages how `kubewatch` handles events. 30 | 31 | With each event get from k8s and matched filtering from configuration, it is passed to handler. Currently, `kubewatch` has 7 handlers: 32 | 33 | - `Default`: which just print the event in JSON format 34 | - `Flock`: which send notification to Flock channel based on information from config 35 | - `Hipchat`: which send notification to Hipchat room based on information from config 36 | - `Mattermost`: which send notification to Mattermost channel based on information from config 37 | - `MS Teams`: which send notification to MS Team incoming webhook based on information from config 38 | - `Slack`: which send notification to Slack channel based on information from config 39 | - `Smtp`: which sends notifications to email recipients using a SMTP server obtained from config 40 | 41 | More handlers will be added in future. 42 | 43 | Each handler must implement the [Handler interface](https://github.com/bitnami-labs/kubewatch/blob/master/pkg/handlers/handler.go#L31) 44 | -------------------------------------------------------------------------------- /docs/kubewatch-logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/kubewatch/f7a55b4c2b54fd27de20183ab1ec1ef7b2756581/docs/kubewatch-logo.jpeg -------------------------------------------------------------------------------- /docs/kubewatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/kubewatch/f7a55b4c2b54fd27de20183ab1ec1ef7b2756581/docs/kubewatch.png -------------------------------------------------------------------------------- /docs/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/kubewatch/f7a55b4c2b54fd27de20183ab1ec1ef7b2756581/docs/slack.png -------------------------------------------------------------------------------- /examples/conf/kubewatch.conf.flock.yaml: -------------------------------------------------------------------------------- 1 | handler: 2 | flock: 3 | url: "https://api.flock.com/hooks/sendMessage/XXXXXXXX" # XXXXXXXX to be replaced with incomming webhooks of the flock channl 4 | resource: 5 | deployment: true 6 | replicationcontroller: false 7 | replicaset: false 8 | daemonset: false 9 | services: false 10 | pod: false 11 | job: false 12 | persistentvolume: false 13 | ingress: false 14 | -------------------------------------------------------------------------------- /examples/conf/kubewatch.conf.hipchat.yaml: -------------------------------------------------------------------------------- 1 | handler: 2 | hipchat: 3 | token: "token" 4 | room: "room_id OR room_name" 5 | url: "https://yourhipchat.domain.com/v2" # defaults to "https://api.hipchat.com/v2" 6 | resource: 7 | deployment: true 8 | replicationcontroller: false 9 | replicaset: false 10 | daemonset: false 11 | services: false 12 | pod: false 13 | job: false 14 | persistentvolume: false 15 | ingress: false 16 | -------------------------------------------------------------------------------- /examples/conf/kubewatch.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "handler": { 3 | "slack": { 4 | "channel": "slack_channel", 5 | "token": "slack_token" 6 | } 7 | }, 8 | "reason": ["Created", "Pulled", "Started"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/conf/kubewatch.conf.mattermost.yaml: -------------------------------------------------------------------------------- 1 | handler: 2 | mattermost: 3 | url: "url of incoming webhook" 4 | channel: "channel" 5 | username: "username" 6 | resource: 7 | deployment: true 8 | replicationcontroller: false 9 | replicaset: false 10 | daemonset: false 11 | services: false 12 | pod: false 13 | job: false 14 | persistentvolume: false 15 | ingress: false 16 | -------------------------------------------------------------------------------- /examples/conf/kubewatch.conf.msteams.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: kubewatch 5 | data: 6 | .kubewatch.yaml: | 7 | namespace: 8 | handler: 9 | msteams: 10 | webhookurl: https://outlook.office.com/webhook/... 11 | resource: 12 | namespace: false 13 | deployment: false 14 | replicationcontroller: false 15 | replicaset: false 16 | daemonset: false 17 | services: false 18 | pod: true 19 | secret: false 20 | configmap: false 21 | -------------------------------------------------------------------------------- /examples/conf/kubewatch.conf.webhook.yaml: -------------------------------------------------------------------------------- 1 | handler: 2 | slack: 3 | token: "" 4 | channel: "" 5 | hipchat: 6 | token: "" 7 | room: "" 8 | url: "" 9 | mattermost: 10 | channel: "" 11 | url: "" 12 | username: "" 13 | flock: 14 | url: "" 15 | webhook: 16 | url: "http://localhost:8080" 17 | resource: 18 | deployment: false 19 | replicationcontroller: false 20 | replicaset: false 21 | daemonset: false 22 | services: false 23 | pod: false 24 | job: false 25 | persistentvolume: false 26 | namespace: true 27 | secret: false 28 | ingress: false 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitnami-labs/kubewatch 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/fatih/structtag v1.2.0 7 | github.com/fsnotify/fsnotify v1.4.9 // indirect 8 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 9 | github.com/golang/protobuf v1.4.2 // indirect 10 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect 11 | github.com/googleapis/gnostic v0.1.0 // indirect 12 | github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb // indirect 13 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 14 | github.com/magiconair/properties v1.7.4 // indirect 15 | github.com/mitchellh/mapstructure v0.0.0-20180111000720-b4575eea38cc // indirect 16 | github.com/mkmik/multierror v0.3.0 17 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 18 | github.com/pelletier/go-toml v1.0.1 // indirect 19 | github.com/segmentio/textio v1.2.0 20 | github.com/sirupsen/logrus v1.6.0 21 | github.com/slack-go/slack v0.6.5 22 | github.com/spf13/cast v1.1.0 // indirect 23 | github.com/spf13/cobra v0.0.1 24 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect 25 | github.com/spf13/viper v1.0.0 26 | github.com/stretchr/testify v1.6.1 // indirect 27 | github.com/tbruyelle/hipchat-go v0.0.0-20160921153256-749fb9e14beb 28 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 // indirect 29 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect 30 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 31 | gopkg.in/yaml.v2 v2.3.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c 33 | k8s.io/api v0.16.8 34 | k8s.io/apimachinery v0.16.8 35 | k8s.io/client-go v0.16.8 36 | ) 37 | -------------------------------------------------------------------------------- /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 | github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 5 | github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= 6 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 7 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 8 | github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 9 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 10 | github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= 11 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 12 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 13 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 14 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 15 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 16 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 21 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 22 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 23 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 24 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 25 | github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= 26 | github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= 27 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 28 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 29 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 30 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 31 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 32 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 33 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 34 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 35 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 36 | github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= 37 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 38 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= 39 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 40 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 41 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 42 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 43 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 44 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 45 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 46 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 48 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 49 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 50 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 51 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 52 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 53 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 54 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 55 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 56 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 57 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 58 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 59 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 60 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 61 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 62 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= 63 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 64 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 65 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 66 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 67 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 68 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 69 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 70 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 71 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 72 | github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI= 73 | github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 74 | github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= 75 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 76 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 77 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 78 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 79 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 80 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 81 | github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb h1:1OvvPvZkn/yCQ3xBcM8y4020wdkMXPHLB4+NfoGWh4U= 82 | github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 83 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 84 | github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 85 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 86 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 87 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 88 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 89 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 90 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 91 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 92 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 93 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 94 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 95 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 96 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 97 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 98 | github.com/magiconair/properties v1.7.4 h1:UVo0TkHGd4lQSN1dVDzs9URCIgReuSIcCXpAVB9nZ80= 99 | github.com/magiconair/properties v1.7.4/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 100 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 101 | github.com/mitchellh/mapstructure v0.0.0-20180111000720-b4575eea38cc h1:5T6hzGUO5OrL6MdYXYoLQtRWJDDgjdlOVBn9mIqGY1g= 102 | github.com/mitchellh/mapstructure v0.0.0-20180111000720-b4575eea38cc/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 103 | github.com/mkmik/multierror v0.3.0 h1:FHr3n5BEVlzlTz8GRbuwimkL2zbdD2gTPcSh0wpRpUg= 104 | github.com/mkmik/multierror v0.3.0/go.mod h1:wjBYXRpDhh+8mIp+iLBOq0kZ3Y4ICTncojwvP8LUYLQ= 105 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 107 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 109 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 110 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 111 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 112 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 113 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 114 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 115 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 116 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 117 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 118 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 119 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 120 | github.com/pelletier/go-toml v1.0.1 h1:0nx4vKBl23+hEaCOV1mFhKS9vhhBtFYWC7rQY0vJAyE= 121 | github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 122 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 123 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 124 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 125 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 126 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 127 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 128 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 129 | github.com/segmentio/textio v1.2.0 h1:Ug4IkV3kh72juJbG8azoSBlgebIbUUxVNrfFcKHfTSQ= 130 | github.com/segmentio/textio v1.2.0/go.mod h1:+Rb7v0YVODP+tK5F7FD9TCkV7gOYx9IgLHWiqtvY8ag= 131 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 132 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 133 | github.com/slack-go/slack v0.6.5 h1:IkDKtJ2IROJNoe3d6mW870/NRKvq2fhLB/Q5XmzWk00= 134 | github.com/slack-go/slack v0.6.5/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM= 135 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 136 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 137 | github.com/spf13/cast v1.1.0 h1:0Rhw4d6C8J9VPu6cjZLIhZ8+aAOHcDvGeKn+cq5Aq3k= 138 | github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= 139 | github.com/spf13/cobra v0.0.1 h1:zZh3X5aZbdnoj+4XkaBxKfhO4ot82icYdhhREIAXIj8= 140 | github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 141 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= 142 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 143 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 144 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 145 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 146 | github.com/spf13/viper v1.0.0 h1:RUA/ghS2i64rlnn4ydTfblY8Og8QzcPtCcHvgMn+w/I= 147 | github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= 148 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 149 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 150 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 151 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 152 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 153 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 154 | github.com/tbruyelle/hipchat-go v0.0.0-20160921153256-749fb9e14beb h1:mb7xv0kx9XpGsLy5kCCa6+3HqSj495cEBQNMgljqZ48= 155 | github.com/tbruyelle/hipchat-go v0.0.0-20160921153256-749fb9e14beb/go.mod h1:CJEWrlDz1qHCF/nywogFd3AqHUWbKCdpu9pSAdf1OzY= 156 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 157 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 158 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 159 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 160 | golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= 161 | golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 162 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 163 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 164 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 165 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 166 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 167 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 168 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 169 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 170 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 171 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 172 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 173 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 174 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= 176 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 177 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 178 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 179 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 180 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 181 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 184 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 185 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 186 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 187 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 188 | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 189 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 190 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 192 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 195 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 196 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= 197 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 198 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 199 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 200 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 201 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 202 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 203 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 204 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 205 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 206 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 207 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 208 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 209 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 210 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 211 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 212 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 213 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 214 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 215 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 216 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 217 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 218 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 219 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 220 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 221 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 222 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 223 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 224 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 225 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 226 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 227 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 228 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 229 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 230 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 231 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 232 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 233 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 234 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 235 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 236 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 237 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 238 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 239 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 240 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 241 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 242 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 243 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 244 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 245 | k8s.io/api v0.16.8 h1:T72itM0CUT8KHqPAqbjTeSY0n24RyVM71nLiMlq/cAw= 246 | k8s.io/api v0.16.8/go.mod h1:a8EOdYHO8en+YHhPBLiW5q+3RfHTr7wxTqqp7emJ7PM= 247 | k8s.io/apimachinery v0.16.8 h1:wgFRqtel3w3rcclpba+iBkVlKeBlh42OzNp7FalXVCg= 248 | k8s.io/apimachinery v0.16.8/go.mod h1:Xk2vD2TRRpuWYLQNM6lT9R7DSFZUYG03SarNkbGrnKE= 249 | k8s.io/client-go v0.16.8 h1:CmsQXJpSWq1aUyQ5Lp/rRPiMK2OYfJv32Ftl0D1D42U= 250 | k8s.io/client-go v0.16.8/go.mod h1:WmPuN0yJTKHXoklExKxzo3jSXmr3EnN+65uaTb5VuNs= 251 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 252 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 253 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 254 | k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= 255 | k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 256 | k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= 257 | k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE= 258 | k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= 259 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 260 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 261 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 262 | -------------------------------------------------------------------------------- /kubewatch-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: kubewatch 5 | data: 6 | .kubewatch.yaml: | 7 | namespace: "" 8 | handler: 9 | slack: 10 | token: 11 | channel: 12 | resource: 13 | deployment: false 14 | replicationcontroller: false 15 | replicaset: false 16 | daemonset: false 17 | services: true 18 | pod: true 19 | secret: false 20 | configmap: false 21 | -------------------------------------------------------------------------------- /kubewatch-in-cluster.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: kubewatch 5 | namespace: default 6 | spec: 7 | containers: 8 | - image: tuna/kubewatch:v0.0.1 9 | imagePullPolicy: Always 10 | name: kubewatch 11 | volumeMounts: 12 | - name: config-volume 13 | mountPath: /root 14 | restartPolicy: Always 15 | volumes: 16 | - name: config-volume 17 | configMap: 18 | name: kubewatch 19 | -------------------------------------------------------------------------------- /kubewatch-service-account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ClusterRole 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: kubewatch 6 | rules: 7 | - apiGroups: [""] 8 | resources: ["pods", "replicationcontrollers"] 9 | verbs: ["get", "watch", "list"] 10 | --- 11 | apiVersion: v1 12 | kind: ServiceAccount 13 | metadata: 14 | name: kubewatch 15 | namespace: default 16 | --- 17 | apiVersion: rbac.authorization.k8s.io/v1beta1 18 | kind: ClusterRoleBinding 19 | metadata: 20 | name: kubewatch 21 | roleRef: 22 | apiGroup: rbac.authorization.k8s.io 23 | kind: ClusterRole 24 | name: kubewatch 25 | subjects: 26 | - kind: ServiceAccount 27 | name: kubewatch 28 | namespace: default 29 | -------------------------------------------------------------------------------- /kubewatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: kubewatch 5 | namespace: default 6 | spec: 7 | containers: 8 | - image: bitnami/kubewatch #using this image, its more stable and active 9 | imagePullPolicy: Always 10 | name: kubewatch 11 | volumeMounts: 12 | - name: config-volume 13 | mountPath: /root 14 | restartPolicy: Always 15 | volumes: 16 | - name: config-volume 17 | configMap: 18 | name: kubewatch -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import "github.com/bitnami-labs/kubewatch/cmd" 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/client/run.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "log" 21 | 22 | "github.com/bitnami-labs/kubewatch/config" 23 | "github.com/bitnami-labs/kubewatch/pkg/controller" 24 | "github.com/bitnami-labs/kubewatch/pkg/handlers" 25 | "github.com/bitnami-labs/kubewatch/pkg/handlers/flock" 26 | "github.com/bitnami-labs/kubewatch/pkg/handlers/hipchat" 27 | "github.com/bitnami-labs/kubewatch/pkg/handlers/mattermost" 28 | "github.com/bitnami-labs/kubewatch/pkg/handlers/msteam" 29 | "github.com/bitnami-labs/kubewatch/pkg/handlers/slack" 30 | "github.com/bitnami-labs/kubewatch/pkg/handlers/smtp" 31 | "github.com/bitnami-labs/kubewatch/pkg/handlers/webhook" 32 | ) 33 | 34 | // Run runs the event loop processing with given handler 35 | func Run(conf *config.Config) { 36 | 37 | var eventHandler = ParseEventHandler(conf) 38 | controller.Start(conf, eventHandler) 39 | } 40 | 41 | // ParseEventHandler returns the respective handler object specified in the config file. 42 | func ParseEventHandler(conf *config.Config) handlers.Handler { 43 | 44 | var eventHandler handlers.Handler 45 | switch { 46 | case len(conf.Handler.Slack.Channel) > 0 || len(conf.Handler.Slack.Token) > 0: 47 | eventHandler = new(slack.Slack) 48 | case len(conf.Handler.Hipchat.Room) > 0 || len(conf.Handler.Hipchat.Token) > 0: 49 | eventHandler = new(hipchat.Hipchat) 50 | case len(conf.Handler.Mattermost.Channel) > 0 || len(conf.Handler.Mattermost.Url) > 0: 51 | eventHandler = new(mattermost.Mattermost) 52 | case len(conf.Handler.Flock.Url) > 0: 53 | eventHandler = new(flock.Flock) 54 | case len(conf.Handler.Webhook.Url) > 0: 55 | eventHandler = new(webhook.Webhook) 56 | case len(conf.Handler.MSTeams.WebhookURL) > 0: 57 | eventHandler = new(msteam.MSTeams) 58 | case len(conf.Handler.SMTP.Smarthost) > 0 || len(conf.Handler.SMTP.To) > 0: 59 | eventHandler = new(smtp.SMTP) 60 | default: 61 | eventHandler = new(handlers.Default) 62 | } 63 | if err := eventHandler.Init(conf); err != nil { 64 | log.Fatal(err) 65 | } 66 | return eventHandler 67 | } 68 | -------------------------------------------------------------------------------- /pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "os/signal" 23 | "strings" 24 | "syscall" 25 | "time" 26 | 27 | "github.com/bitnami-labs/kubewatch/config" 28 | "github.com/bitnami-labs/kubewatch/pkg/event" 29 | "github.com/bitnami-labs/kubewatch/pkg/handlers" 30 | "github.com/bitnami-labs/kubewatch/pkg/utils" 31 | "github.com/sirupsen/logrus" 32 | 33 | apps_v1 "k8s.io/api/apps/v1" 34 | batch_v1 "k8s.io/api/batch/v1" 35 | api_v1 "k8s.io/api/core/v1" 36 | ext_v1beta1 "k8s.io/api/extensions/v1beta1" 37 | rbac_v1beta1 "k8s.io/api/rbac/v1beta1" 38 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39 | "k8s.io/apimachinery/pkg/runtime" 40 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 41 | "k8s.io/apimachinery/pkg/util/wait" 42 | "k8s.io/apimachinery/pkg/watch" 43 | "k8s.io/client-go/kubernetes" 44 | "k8s.io/client-go/rest" 45 | "k8s.io/client-go/tools/cache" 46 | "k8s.io/client-go/util/workqueue" 47 | ) 48 | 49 | const maxRetries = 5 50 | 51 | var serverStartTime time.Time 52 | 53 | // Event indicate the informerEvent 54 | type Event struct { 55 | key string 56 | eventType string 57 | namespace string 58 | resourceType string 59 | } 60 | 61 | // Controller object 62 | type Controller struct { 63 | logger *logrus.Entry 64 | clientset kubernetes.Interface 65 | queue workqueue.RateLimitingInterface 66 | informer cache.SharedIndexInformer 67 | eventHandler handlers.Handler 68 | } 69 | 70 | // Start prepares watchers and run their controllers, then waits for process termination signals 71 | func Start(conf *config.Config, eventHandler handlers.Handler) { 72 | var kubeClient kubernetes.Interface 73 | 74 | if _, err := rest.InClusterConfig(); err != nil { 75 | kubeClient = utils.GetClientOutOfCluster() 76 | } else { 77 | kubeClient = utils.GetClient() 78 | } 79 | 80 | // Adding Default Critical Alerts 81 | // For Capturing Critical Event NodeNotReady in Nodes 82 | nodeNotReadyInformer := cache.NewSharedIndexInformer( 83 | &cache.ListWatch{ 84 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 85 | options.FieldSelector = "involvedObject.kind=Node,type=Normal,reason=NodeNotReady" 86 | return kubeClient.CoreV1().Events(conf.Namespace).List(options) 87 | }, 88 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 89 | options.FieldSelector = "involvedObject.kind=Node,type=Normal,reason=NodeNotReady" 90 | return kubeClient.CoreV1().Events(conf.Namespace).Watch(options) 91 | }, 92 | }, 93 | &api_v1.Event{}, 94 | 0, //Skip resync 95 | cache.Indexers{}, 96 | ) 97 | 98 | nodeNotReadyController := newResourceController(kubeClient, eventHandler, nodeNotReadyInformer, "NodeNotReady") 99 | stopNodeNotReadyCh := make(chan struct{}) 100 | defer close(stopNodeNotReadyCh) 101 | 102 | go nodeNotReadyController.Run(stopNodeNotReadyCh) 103 | 104 | // For Capturing Critical Event NodeReady in Nodes 105 | nodeReadyInformer := cache.NewSharedIndexInformer( 106 | &cache.ListWatch{ 107 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 108 | options.FieldSelector = "involvedObject.kind=Node,type=Normal,reason=NodeReady" 109 | return kubeClient.CoreV1().Events(conf.Namespace).List(options) 110 | }, 111 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 112 | options.FieldSelector = "involvedObject.kind=Node,type=Normal,reason=NodeReady" 113 | return kubeClient.CoreV1().Events(conf.Namespace).Watch(options) 114 | }, 115 | }, 116 | &api_v1.Event{}, 117 | 0, //Skip resync 118 | cache.Indexers{}, 119 | ) 120 | 121 | nodeReadyController := newResourceController(kubeClient, eventHandler, nodeReadyInformer, "NodeReady") 122 | stopNodeReadyCh := make(chan struct{}) 123 | defer close(stopNodeReadyCh) 124 | 125 | go nodeReadyController.Run(stopNodeReadyCh) 126 | 127 | // For Capturing Critical Event NodeRebooted in Nodes 128 | nodeRebootedInformer := cache.NewSharedIndexInformer( 129 | &cache.ListWatch{ 130 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 131 | options.FieldSelector = "involvedObject.kind=Node,type=Warning,reason=Rebooted" 132 | return kubeClient.CoreV1().Events(conf.Namespace).List(options) 133 | }, 134 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 135 | options.FieldSelector = "involvedObject.kind=Node,type=Warning,reason=Rebooted" 136 | return kubeClient.CoreV1().Events(conf.Namespace).Watch(options) 137 | }, 138 | }, 139 | &api_v1.Event{}, 140 | 0, //Skip resync 141 | cache.Indexers{}, 142 | ) 143 | 144 | nodeRebootedController := newResourceController(kubeClient, eventHandler, nodeRebootedInformer, "NodeRebooted") 145 | stopNodeRebootedCh := make(chan struct{}) 146 | defer close(stopNodeRebootedCh) 147 | 148 | go nodeRebootedController.Run(stopNodeRebootedCh) 149 | 150 | // User Configured Events 151 | if conf.Resource.Pod { 152 | informer := cache.NewSharedIndexInformer( 153 | &cache.ListWatch{ 154 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 155 | return kubeClient.CoreV1().Pods(conf.Namespace).List(options) 156 | }, 157 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 158 | return kubeClient.CoreV1().Pods(conf.Namespace).Watch(options) 159 | }, 160 | }, 161 | &api_v1.Pod{}, 162 | 0, //Skip resync 163 | cache.Indexers{}, 164 | ) 165 | 166 | c := newResourceController(kubeClient, eventHandler, informer, "pod") 167 | stopCh := make(chan struct{}) 168 | defer close(stopCh) 169 | 170 | go c.Run(stopCh) 171 | 172 | // For Capturing CrashLoopBackOff Events in pods 173 | backoffInformer := cache.NewSharedIndexInformer( 174 | &cache.ListWatch{ 175 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 176 | options.FieldSelector = "involvedObject.kind=Pod,type=Warning,reason=BackOff" 177 | return kubeClient.CoreV1().Events(conf.Namespace).List(options) 178 | }, 179 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 180 | options.FieldSelector = "involvedObject.kind=Pod,type=Warning,reason=BackOff" 181 | return kubeClient.CoreV1().Events(conf.Namespace).Watch(options) 182 | }, 183 | }, 184 | &api_v1.Event{}, 185 | 0, //Skip resync 186 | cache.Indexers{}, 187 | ) 188 | 189 | backoffcontroller := newResourceController(kubeClient, eventHandler, backoffInformer, "Backoff") 190 | stopBackoffCh := make(chan struct{}) 191 | defer close(stopBackoffCh) 192 | 193 | go backoffcontroller.Run(stopBackoffCh) 194 | 195 | } 196 | 197 | if conf.Resource.DaemonSet { 198 | informer := cache.NewSharedIndexInformer( 199 | &cache.ListWatch{ 200 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 201 | return kubeClient.AppsV1().DaemonSets(conf.Namespace).List(options) 202 | }, 203 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 204 | return kubeClient.AppsV1().DaemonSets(conf.Namespace).Watch(options) 205 | }, 206 | }, 207 | &apps_v1.DaemonSet{}, 208 | 0, //Skip resync 209 | cache.Indexers{}, 210 | ) 211 | 212 | c := newResourceController(kubeClient, eventHandler, informer, "daemon set") 213 | stopCh := make(chan struct{}) 214 | defer close(stopCh) 215 | 216 | go c.Run(stopCh) 217 | } 218 | 219 | if conf.Resource.ReplicaSet { 220 | informer := cache.NewSharedIndexInformer( 221 | &cache.ListWatch{ 222 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 223 | return kubeClient.AppsV1().ReplicaSets(conf.Namespace).List(options) 224 | }, 225 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 226 | return kubeClient.AppsV1().ReplicaSets(conf.Namespace).Watch(options) 227 | }, 228 | }, 229 | &apps_v1.ReplicaSet{}, 230 | 0, //Skip resync 231 | cache.Indexers{}, 232 | ) 233 | 234 | c := newResourceController(kubeClient, eventHandler, informer, "replica set") 235 | stopCh := make(chan struct{}) 236 | defer close(stopCh) 237 | 238 | go c.Run(stopCh) 239 | } 240 | 241 | if conf.Resource.Services { 242 | informer := cache.NewSharedIndexInformer( 243 | &cache.ListWatch{ 244 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 245 | return kubeClient.CoreV1().Services(conf.Namespace).List(options) 246 | }, 247 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 248 | return kubeClient.CoreV1().Services(conf.Namespace).Watch(options) 249 | }, 250 | }, 251 | &api_v1.Service{}, 252 | 0, //Skip resync 253 | cache.Indexers{}, 254 | ) 255 | 256 | c := newResourceController(kubeClient, eventHandler, informer, "service") 257 | stopCh := make(chan struct{}) 258 | defer close(stopCh) 259 | 260 | go c.Run(stopCh) 261 | } 262 | 263 | if conf.Resource.Deployment { 264 | informer := cache.NewSharedIndexInformer( 265 | &cache.ListWatch{ 266 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 267 | return kubeClient.AppsV1().Deployments(conf.Namespace).List(options) 268 | }, 269 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 270 | return kubeClient.AppsV1().Deployments(conf.Namespace).Watch(options) 271 | }, 272 | }, 273 | &apps_v1.Deployment{}, 274 | 0, //Skip resync 275 | cache.Indexers{}, 276 | ) 277 | 278 | c := newResourceController(kubeClient, eventHandler, informer, "deployment") 279 | stopCh := make(chan struct{}) 280 | defer close(stopCh) 281 | 282 | go c.Run(stopCh) 283 | } 284 | 285 | if conf.Resource.Namespace { 286 | informer := cache.NewSharedIndexInformer( 287 | &cache.ListWatch{ 288 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 289 | return kubeClient.CoreV1().Namespaces().List(options) 290 | }, 291 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 292 | return kubeClient.CoreV1().Namespaces().Watch(options) 293 | }, 294 | }, 295 | &api_v1.Namespace{}, 296 | 0, //Skip resync 297 | cache.Indexers{}, 298 | ) 299 | 300 | c := newResourceController(kubeClient, eventHandler, informer, "namespace") 301 | stopCh := make(chan struct{}) 302 | defer close(stopCh) 303 | 304 | go c.Run(stopCh) 305 | } 306 | 307 | if conf.Resource.ReplicationController { 308 | informer := cache.NewSharedIndexInformer( 309 | &cache.ListWatch{ 310 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 311 | return kubeClient.CoreV1().ReplicationControllers(conf.Namespace).List(options) 312 | }, 313 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 314 | return kubeClient.CoreV1().ReplicationControllers(conf.Namespace).Watch(options) 315 | }, 316 | }, 317 | &api_v1.ReplicationController{}, 318 | 0, //Skip resync 319 | cache.Indexers{}, 320 | ) 321 | 322 | c := newResourceController(kubeClient, eventHandler, informer, "replication controller") 323 | stopCh := make(chan struct{}) 324 | defer close(stopCh) 325 | 326 | go c.Run(stopCh) 327 | } 328 | 329 | if conf.Resource.Job { 330 | informer := cache.NewSharedIndexInformer( 331 | &cache.ListWatch{ 332 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 333 | return kubeClient.BatchV1().Jobs(conf.Namespace).List(options) 334 | }, 335 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 336 | return kubeClient.BatchV1().Jobs(conf.Namespace).Watch(options) 337 | }, 338 | }, 339 | &batch_v1.Job{}, 340 | 0, //Skip resync 341 | cache.Indexers{}, 342 | ) 343 | 344 | c := newResourceController(kubeClient, eventHandler, informer, "job") 345 | stopCh := make(chan struct{}) 346 | defer close(stopCh) 347 | 348 | go c.Run(stopCh) 349 | } 350 | 351 | if conf.Resource.Node { 352 | informer := cache.NewSharedIndexInformer( 353 | &cache.ListWatch{ 354 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 355 | return kubeClient.CoreV1().Nodes().List(options) 356 | }, 357 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 358 | return kubeClient.CoreV1().Nodes().Watch(options) 359 | }, 360 | }, 361 | &api_v1.Node{}, 362 | 0, //Skip resync 363 | cache.Indexers{}, 364 | ) 365 | 366 | c := newResourceController(kubeClient, eventHandler, informer, "node") 367 | stopCh := make(chan struct{}) 368 | defer close(stopCh) 369 | 370 | go c.Run(stopCh) 371 | } 372 | 373 | if conf.Resource.ServiceAccount { 374 | informer := cache.NewSharedIndexInformer( 375 | &cache.ListWatch{ 376 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 377 | return kubeClient.CoreV1().ServiceAccounts(conf.Namespace).List(options) 378 | }, 379 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 380 | return kubeClient.CoreV1().ServiceAccounts(conf.Namespace).Watch(options) 381 | }, 382 | }, 383 | &api_v1.ServiceAccount{}, 384 | 0, //Skip resync 385 | cache.Indexers{}, 386 | ) 387 | 388 | c := newResourceController(kubeClient, eventHandler, informer, "service account") 389 | stopCh := make(chan struct{}) 390 | defer close(stopCh) 391 | 392 | go c.Run(stopCh) 393 | } 394 | 395 | if conf.Resource.ClusterRole { 396 | informer := cache.NewSharedIndexInformer( 397 | &cache.ListWatch{ 398 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 399 | return kubeClient.RbacV1beta1().ClusterRoles().List(options) 400 | }, 401 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 402 | return kubeClient.RbacV1beta1().ClusterRoles().Watch(options) 403 | }, 404 | }, 405 | &rbac_v1beta1.ClusterRole{}, 406 | 0, //Skip resync 407 | cache.Indexers{}, 408 | ) 409 | 410 | c := newResourceController(kubeClient, eventHandler, informer, "cluster role") 411 | stopCh := make(chan struct{}) 412 | defer close(stopCh) 413 | 414 | go c.Run(stopCh) 415 | } 416 | 417 | if conf.Resource.PersistentVolume { 418 | informer := cache.NewSharedIndexInformer( 419 | &cache.ListWatch{ 420 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 421 | return kubeClient.CoreV1().PersistentVolumes().List(options) 422 | }, 423 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 424 | return kubeClient.CoreV1().PersistentVolumes().Watch(options) 425 | }, 426 | }, 427 | &api_v1.PersistentVolume{}, 428 | 0, //Skip resync 429 | cache.Indexers{}, 430 | ) 431 | 432 | c := newResourceController(kubeClient, eventHandler, informer, "persistent volume") 433 | stopCh := make(chan struct{}) 434 | defer close(stopCh) 435 | 436 | go c.Run(stopCh) 437 | } 438 | 439 | if conf.Resource.Secret { 440 | informer := cache.NewSharedIndexInformer( 441 | &cache.ListWatch{ 442 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 443 | return kubeClient.CoreV1().Secrets(conf.Namespace).List(options) 444 | }, 445 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 446 | return kubeClient.CoreV1().Secrets(conf.Namespace).Watch(options) 447 | }, 448 | }, 449 | &api_v1.Secret{}, 450 | 0, //Skip resync 451 | cache.Indexers{}, 452 | ) 453 | 454 | c := newResourceController(kubeClient, eventHandler, informer, "secret") 455 | stopCh := make(chan struct{}) 456 | defer close(stopCh) 457 | 458 | go c.Run(stopCh) 459 | } 460 | 461 | if conf.Resource.ConfigMap { 462 | informer := cache.NewSharedIndexInformer( 463 | &cache.ListWatch{ 464 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 465 | return kubeClient.CoreV1().ConfigMaps(conf.Namespace).List(options) 466 | }, 467 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 468 | return kubeClient.CoreV1().ConfigMaps(conf.Namespace).Watch(options) 469 | }, 470 | }, 471 | &api_v1.ConfigMap{}, 472 | 0, //Skip resync 473 | cache.Indexers{}, 474 | ) 475 | 476 | c := newResourceController(kubeClient, eventHandler, informer, "configmap") 477 | stopCh := make(chan struct{}) 478 | defer close(stopCh) 479 | 480 | go c.Run(stopCh) 481 | } 482 | 483 | if conf.Resource.Ingress { 484 | informer := cache.NewSharedIndexInformer( 485 | &cache.ListWatch{ 486 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 487 | return kubeClient.ExtensionsV1beta1().Ingresses(conf.Namespace).List(options) 488 | }, 489 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 490 | return kubeClient.ExtensionsV1beta1().Ingresses(conf.Namespace).Watch(options) 491 | }, 492 | }, 493 | &ext_v1beta1.Ingress{}, 494 | 0, //Skip resync 495 | cache.Indexers{}, 496 | ) 497 | 498 | c := newResourceController(kubeClient, eventHandler, informer, "ingress") 499 | stopCh := make(chan struct{}) 500 | defer close(stopCh) 501 | 502 | go c.Run(stopCh) 503 | } 504 | 505 | sigterm := make(chan os.Signal, 1) 506 | signal.Notify(sigterm, syscall.SIGTERM) 507 | signal.Notify(sigterm, syscall.SIGINT) 508 | <-sigterm 509 | } 510 | 511 | func newResourceController(client kubernetes.Interface, eventHandler handlers.Handler, informer cache.SharedIndexInformer, resourceType string) *Controller { 512 | queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) 513 | var newEvent Event 514 | var err error 515 | informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 516 | AddFunc: func(obj interface{}) { 517 | newEvent.key, err = cache.MetaNamespaceKeyFunc(obj) 518 | newEvent.eventType = "create" 519 | newEvent.resourceType = resourceType 520 | logrus.WithField("pkg", "kubewatch-"+resourceType).Infof("Processing add to %v: %s", resourceType, newEvent.key) 521 | if err == nil { 522 | queue.Add(newEvent) 523 | } 524 | }, 525 | UpdateFunc: func(old, new interface{}) { 526 | newEvent.key, err = cache.MetaNamespaceKeyFunc(old) 527 | newEvent.eventType = "update" 528 | newEvent.resourceType = resourceType 529 | logrus.WithField("pkg", "kubewatch-"+resourceType).Infof("Processing update to %v: %s", resourceType, newEvent.key) 530 | if err == nil { 531 | queue.Add(newEvent) 532 | } 533 | }, 534 | DeleteFunc: func(obj interface{}) { 535 | newEvent.key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj) 536 | newEvent.eventType = "delete" 537 | newEvent.resourceType = resourceType 538 | newEvent.namespace = utils.GetObjectMetaData(obj).Namespace 539 | logrus.WithField("pkg", "kubewatch-"+resourceType).Infof("Processing delete to %v: %s", resourceType, newEvent.key) 540 | if err == nil { 541 | queue.Add(newEvent) 542 | } 543 | }, 544 | }) 545 | 546 | return &Controller{ 547 | logger: logrus.WithField("pkg", "kubewatch-"+resourceType), 548 | clientset: client, 549 | informer: informer, 550 | queue: queue, 551 | eventHandler: eventHandler, 552 | } 553 | } 554 | 555 | // Run starts the kubewatch controller 556 | func (c *Controller) Run(stopCh <-chan struct{}) { 557 | defer utilruntime.HandleCrash() 558 | defer c.queue.ShutDown() 559 | 560 | c.logger.Info("Starting kubewatch controller") 561 | serverStartTime = time.Now().Local() 562 | 563 | go c.informer.Run(stopCh) 564 | 565 | if !cache.WaitForCacheSync(stopCh, c.HasSynced) { 566 | utilruntime.HandleError(fmt.Errorf("Timed out waiting for caches to sync")) 567 | return 568 | } 569 | 570 | c.logger.Info("Kubewatch controller synced and ready") 571 | 572 | wait.Until(c.runWorker, time.Second, stopCh) 573 | } 574 | 575 | // HasSynced is required for the cache.Controller interface. 576 | func (c *Controller) HasSynced() bool { 577 | return c.informer.HasSynced() 578 | } 579 | 580 | // LastSyncResourceVersion is required for the cache.Controller interface. 581 | func (c *Controller) LastSyncResourceVersion() string { 582 | return c.informer.LastSyncResourceVersion() 583 | } 584 | 585 | func (c *Controller) runWorker() { 586 | for c.processNextItem() { 587 | // continue looping 588 | } 589 | } 590 | 591 | func (c *Controller) processNextItem() bool { 592 | newEvent, quit := c.queue.Get() 593 | 594 | if quit { 595 | return false 596 | } 597 | defer c.queue.Done(newEvent) 598 | err := c.processItem(newEvent.(Event)) 599 | if err == nil { 600 | // No error, reset the ratelimit counters 601 | c.queue.Forget(newEvent) 602 | } else if c.queue.NumRequeues(newEvent) < maxRetries { 603 | c.logger.Errorf("Error processing %s (will retry): %v", newEvent.(Event).key, err) 604 | c.queue.AddRateLimited(newEvent) 605 | } else { 606 | // err != nil and too many retries 607 | c.logger.Errorf("Error processing %s (giving up): %v", newEvent.(Event).key, err) 608 | c.queue.Forget(newEvent) 609 | utilruntime.HandleError(err) 610 | } 611 | 612 | return true 613 | } 614 | 615 | /* TODOs 616 | - Enhance event creation using client-side cacheing machanisms - pending 617 | - Enhance the processItem to classify events - done 618 | - Send alerts correspoding to events - done 619 | */ 620 | 621 | func (c *Controller) processItem(newEvent Event) error { 622 | obj, _, err := c.informer.GetIndexer().GetByKey(newEvent.key) 623 | if err != nil { 624 | return fmt.Errorf("Error fetching object with key %s from store: %v", newEvent.key, err) 625 | } 626 | // get object's metedata 627 | objectMeta := utils.GetObjectMetaData(obj) 628 | 629 | // hold status type for default critical alerts 630 | var status string 631 | 632 | // namespace retrived from event key incase namespace value is empty 633 | if newEvent.namespace == "" && strings.Contains(newEvent.key, "/") { 634 | substring := strings.Split(newEvent.key, "/") 635 | newEvent.namespace = substring[0] 636 | newEvent.key = substring[1] 637 | } 638 | 639 | // process events based on its type 640 | switch newEvent.eventType { 641 | case "create": 642 | // compare CreationTimestamp and serverStartTime and alert only on latest events 643 | // Could be Replaced by using Delta or DeltaFIFO 644 | if objectMeta.CreationTimestamp.Sub(serverStartTime).Seconds() > 0 { 645 | switch newEvent.resourceType { 646 | case "NodeNotReady": 647 | status = "Danger" 648 | case "NodeReady": 649 | status = "Normal" 650 | case "NodeRebooted": 651 | status = "Danger" 652 | case "Backoff": 653 | status = "Danger" 654 | default: 655 | status = "Normal" 656 | } 657 | kbEvent := event.Event{ 658 | Name: objectMeta.Name, 659 | Namespace: newEvent.namespace, 660 | Kind: newEvent.resourceType, 661 | Status: status, 662 | Reason: "Created", 663 | } 664 | c.eventHandler.Handle(kbEvent) 665 | return nil 666 | } 667 | case "update": 668 | /* TODOs 669 | - enahace update event processing in such a way that, it send alerts about what got changed. 670 | */ 671 | switch newEvent.resourceType { 672 | case "Backoff": 673 | status = "Danger" 674 | default: 675 | status = "Warning" 676 | } 677 | kbEvent := event.Event{ 678 | Name: newEvent.key, 679 | Namespace: newEvent.namespace, 680 | Kind: newEvent.resourceType, 681 | Status: status, 682 | Reason: "Updated", 683 | } 684 | c.eventHandler.Handle(kbEvent) 685 | return nil 686 | case "delete": 687 | kbEvent := event.Event{ 688 | Name: newEvent.key, 689 | Namespace: newEvent.namespace, 690 | Kind: newEvent.resourceType, 691 | Status: "Danger", 692 | Reason: "Deleted", 693 | } 694 | c.eventHandler.Handle(kbEvent) 695 | return nil 696 | } 697 | return nil 698 | } 699 | -------------------------------------------------------------------------------- /pkg/event/event.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package event 15 | 16 | import ( 17 | "fmt" 18 | 19 | "github.com/bitnami-labs/kubewatch/pkg/utils" 20 | apps_v1 "k8s.io/api/apps/v1" 21 | batch_v1 "k8s.io/api/batch/v1" 22 | api_v1 "k8s.io/api/core/v1" 23 | ext_v1beta1 "k8s.io/api/extensions/v1beta1" 24 | rbac_v1beta1 "k8s.io/api/rbac/v1beta1" 25 | ) 26 | 27 | // Event represent an event got from k8s api server 28 | // Events from different endpoints need to be casted to KubewatchEvent 29 | // before being able to be handled by handler 30 | type Event struct { 31 | Namespace string 32 | Kind string 33 | Component string 34 | Host string 35 | Reason string 36 | Status string 37 | Name string 38 | } 39 | 40 | var m = map[string]string{ 41 | "created": "Normal", 42 | "deleted": "Danger", 43 | "updated": "Warning", 44 | } 45 | 46 | // New create new KubewatchEvent 47 | func New(obj interface{}, action string) Event { 48 | var namespace, kind, component, host, reason, status, name string 49 | 50 | objectMeta := utils.GetObjectMetaData(obj) 51 | namespace = objectMeta.Namespace 52 | name = objectMeta.Name 53 | reason = action 54 | status = m[action] 55 | 56 | switch object := obj.(type) { 57 | case *ext_v1beta1.DaemonSet: 58 | kind = "daemon set" 59 | case *apps_v1.Deployment: 60 | kind = "deployment" 61 | case *batch_v1.Job: 62 | kind = "job" 63 | case *api_v1.Namespace: 64 | kind = "namespace" 65 | case *ext_v1beta1.Ingress: 66 | kind = "ingress" 67 | case *api_v1.PersistentVolume: 68 | kind = "persistent volume" 69 | case *api_v1.Pod: 70 | kind = "pod" 71 | host = object.Spec.NodeName 72 | case *api_v1.ReplicationController: 73 | kind = "replication controller" 74 | case *ext_v1beta1.ReplicaSet: 75 | kind = "replica set" 76 | case *api_v1.Service: 77 | kind = "service" 78 | component = string(object.Spec.Type) 79 | case *api_v1.Secret: 80 | kind = "secret" 81 | case *api_v1.ConfigMap: 82 | kind = "configmap" 83 | case *api_v1.Node: 84 | kind = "node" 85 | case *rbac_v1beta1.ClusterRole: 86 | kind = "cluster role" 87 | case *api_v1.ServiceAccount: 88 | kind = "service account" 89 | case Event: 90 | name = object.Name 91 | kind = object.Kind 92 | namespace = object.Namespace 93 | } 94 | 95 | kbEvent := Event{ 96 | Namespace: namespace, 97 | Kind: kind, 98 | Component: component, 99 | Host: host, 100 | Reason: reason, 101 | Status: status, 102 | Name: name, 103 | } 104 | return kbEvent 105 | } 106 | 107 | // Message returns event message in standard format. 108 | // included as a part of event packege to enhance code resuablity across handlers. 109 | func (e *Event) Message() (msg string) { 110 | // using switch over if..else, since the format could vary based on the kind of the object in future. 111 | switch e.Kind { 112 | case "namespace": 113 | msg = fmt.Sprintf( 114 | "A namespace `%s` has been `%s`", 115 | e.Name, 116 | e.Reason, 117 | ) 118 | case "node": 119 | msg = fmt.Sprintf( 120 | "A node `%s` has been `%s`", 121 | e.Name, 122 | e.Reason, 123 | ) 124 | case "cluster role": 125 | msg = fmt.Sprintf( 126 | "A cluster role `%s` has been `%s`", 127 | e.Name, 128 | e.Reason, 129 | ) 130 | case "NodeReady": 131 | msg = fmt.Sprintf( 132 | "Node `%s` is Ready : \nNodeReady", 133 | e.Name, 134 | ) 135 | case "NodeNotReady": 136 | msg = fmt.Sprintf( 137 | "Node `%s` is Not Ready : \nNodeNotReady", 138 | e.Name, 139 | ) 140 | case "NodeRebooted": 141 | msg = fmt.Sprintf( 142 | "Node `%s` Rebooted : \nNodeRebooted", 143 | e.Name, 144 | ) 145 | case "Backoff": 146 | msg = fmt.Sprintf( 147 | "Pod `%s` in `%s` Crashed : \nCrashLoopBackOff %s", 148 | e.Name, 149 | e.Namespace, 150 | e.Reason, 151 | ) 152 | default: 153 | msg = fmt.Sprintf( 154 | "A `%s` in namespace `%s` has been `%s`:\n`%s`", 155 | e.Kind, 156 | e.Namespace, 157 | e.Reason, 158 | e.Name, 159 | ) 160 | } 161 | return msg 162 | } 163 | -------------------------------------------------------------------------------- /pkg/handlers/flock/flock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package flock 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "os" 23 | 24 | "bytes" 25 | "encoding/json" 26 | "net/http" 27 | "time" 28 | 29 | "github.com/bitnami-labs/kubewatch/config" 30 | "github.com/bitnami-labs/kubewatch/pkg/event" 31 | ) 32 | 33 | var flockColors = map[string]string{ 34 | "Normal": "#00FF00", 35 | "Warning": "#FFFF00", 36 | "Danger": "#FF0000", 37 | } 38 | 39 | var flockErrMsg = ` 40 | %s 41 | 42 | You need to set Flock url for Flock notify, 43 | using "--url/-u" or using environment variables: 44 | 45 | export KW_FLOCK_URL=flock_url 46 | 47 | Command line flags will override environment variables 48 | 49 | ` 50 | 51 | // Flock handler implements handler.Handler interface, 52 | // Notify event to Flock channel 53 | type Flock struct { 54 | Url string 55 | } 56 | 57 | // FlockMessage struct 58 | type FlockMessage struct { 59 | Notification string `json:"notification"` 60 | Text string `json:"text"` 61 | Attachements []FlockMessageAttachement `json:"attachments"` 62 | } 63 | 64 | // FlockMessageAttachement struct 65 | type FlockMessageAttachement struct { 66 | Title string `json:"title"` 67 | Color string `json:"color"` 68 | Views FlockMessageAttachementViews `json:"views"` 69 | } 70 | 71 | // FlockMessageAttachementViews struct 72 | type FlockMessageAttachementViews struct { 73 | Flockml string `json:"flockml"` 74 | } 75 | 76 | // Init prepares Flock configuration 77 | func (f *Flock) Init(c *config.Config) error { 78 | url := c.Handler.Flock.Url 79 | 80 | if url == "" { 81 | url = os.Getenv("KW_FLOCK_URL") 82 | } 83 | 84 | f.Url = url 85 | 86 | return checkMissingFlockVars(f) 87 | } 88 | 89 | // Handle handles an event. 90 | func (f *Flock) Handle(e event.Event) { 91 | flockMessage := prepareFlockMessage(e, f) 92 | 93 | err := postMessage(f.Url, flockMessage) 94 | if err != nil { 95 | log.Printf("%s\n", err) 96 | return 97 | } 98 | 99 | log.Printf("Message successfully sent to channel %s at %s", f.Url, time.Now()) 100 | } 101 | 102 | func checkMissingFlockVars(s *Flock) error { 103 | if s.Url == "" { 104 | return fmt.Errorf(flockErrMsg, "Missing Flock url") 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func prepareFlockMessage(e event.Event, f *Flock) *FlockMessage { 111 | return &FlockMessage{ 112 | Text: "Kubewatch Alert", 113 | Notification: "Kubewatch Alert", 114 | Attachements: []FlockMessageAttachement{ 115 | { 116 | Title: e.Message(), 117 | Color: flockColors[e.Status], 118 | }, 119 | }, 120 | } 121 | } 122 | 123 | func postMessage(url string, flockMessage *FlockMessage) error { 124 | message, err := json.Marshal(flockMessage) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(message)) 130 | if err != nil { 131 | return err 132 | } 133 | req.Header.Add("Content-Type", "application/json") 134 | 135 | client := &http.Client{} 136 | _, err = client.Do(req) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /pkg/handlers/flock/flock_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package flock 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | "github.com/bitnami-labs/kubewatch/config" 25 | ) 26 | 27 | func TestFlockInit(t *testing.T) { 28 | s := &Flock{} 29 | expectedError := fmt.Errorf(flockErrMsg, "Missing Flock url") 30 | 31 | var Tests = []struct { 32 | flock config.Flock 33 | err error 34 | }{ 35 | {config.Flock{Url: "foo"}, nil}, 36 | {config.Flock{}, expectedError}, 37 | } 38 | 39 | for _, tt := range Tests { 40 | c := &config.Config{} 41 | c.Handler.Flock = tt.flock 42 | if err := s.Init(c); !reflect.DeepEqual(err, tt.err) { 43 | t.Fatalf("Init(): %v", err) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/handlers/handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package handlers 18 | 19 | import ( 20 | "github.com/bitnami-labs/kubewatch/config" 21 | "github.com/bitnami-labs/kubewatch/pkg/event" 22 | "github.com/bitnami-labs/kubewatch/pkg/handlers/flock" 23 | "github.com/bitnami-labs/kubewatch/pkg/handlers/hipchat" 24 | "github.com/bitnami-labs/kubewatch/pkg/handlers/mattermost" 25 | "github.com/bitnami-labs/kubewatch/pkg/handlers/msteam" 26 | "github.com/bitnami-labs/kubewatch/pkg/handlers/slack" 27 | "github.com/bitnami-labs/kubewatch/pkg/handlers/smtp" 28 | "github.com/bitnami-labs/kubewatch/pkg/handlers/webhook" 29 | ) 30 | 31 | // Handler is implemented by any handler. 32 | // The Handle method is used to process event 33 | type Handler interface { 34 | Init(c *config.Config) error 35 | Handle(e event.Event) 36 | } 37 | 38 | // Map maps each event handler function to a name for easily lookup 39 | var Map = map[string]interface{}{ 40 | "default": &Default{}, 41 | "slack": &slack.Slack{}, 42 | "hipchat": &hipchat.Hipchat{}, 43 | "mattermost": &mattermost.Mattermost{}, 44 | "flock": &flock.Flock{}, 45 | "webhook": &webhook.Webhook{}, 46 | "ms-teams": &msteam.MSTeams{}, 47 | "smtp": &smtp.SMTP{}, 48 | } 49 | 50 | // Default handler implements Handler interface, 51 | // print each event with JSON format 52 | type Default struct { 53 | } 54 | 55 | // Init initializes handler configuration 56 | // Do nothing for default handler 57 | func (d *Default) Init(c *config.Config) error { 58 | return nil 59 | } 60 | 61 | // Handle handles an event. 62 | func (d *Default) Handle(e event.Event) { 63 | } 64 | -------------------------------------------------------------------------------- /pkg/handlers/hipchat/hipchat.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package hipchat 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "os" 23 | 24 | hipchat "github.com/tbruyelle/hipchat-go/hipchat" 25 | 26 | "net/url" 27 | 28 | "github.com/bitnami-labs/kubewatch/config" 29 | "github.com/bitnami-labs/kubewatch/pkg/event" 30 | ) 31 | 32 | var hipchatColors = map[string]hipchat.Color{ 33 | "Normal": hipchat.ColorGreen, 34 | "Warning": hipchat.ColorYellow, 35 | "Danger": hipchat.ColorRed, 36 | } 37 | 38 | var hipchatErrMsg = ` 39 | %s 40 | 41 | You need to set both hipchat token and room for hipchat notify, 42 | using "--token/-t", "--room/-r", and "--url/-u" or using environment variables: 43 | 44 | export KW_HIPCHAT_TOKEN=hipchat_token 45 | export KW_HIPCHAT_ROOM=hipchat_room 46 | export KW_HIPCHAT_URL=hipchat_url (defaults to https://api.hipchat.com/v2) 47 | 48 | Command line flags will override environment variables 49 | 50 | ` 51 | 52 | // Hipchat handler implements handler.Handler interface, 53 | // Notify event to hipchat room 54 | type Hipchat struct { 55 | Token string 56 | Room string 57 | Url string 58 | } 59 | 60 | // Init prepares hipchat configuration 61 | func (s *Hipchat) Init(c *config.Config) error { 62 | url := c.Handler.Hipchat.Url 63 | room := c.Handler.Hipchat.Room 64 | token := c.Handler.Hipchat.Token 65 | 66 | if token == "" { 67 | token = os.Getenv("KW_HIPCHAT_TOKEN") 68 | } 69 | 70 | if room == "" { 71 | room = os.Getenv("KW_HIPCHAT_ROOM") 72 | } 73 | 74 | if url == "" { 75 | url = os.Getenv("KW_HIPCHAT_URL") 76 | } 77 | 78 | s.Token = token 79 | s.Room = room 80 | s.Url = url 81 | 82 | return checkMissingHipchatVars(s) 83 | } 84 | 85 | // Handle handles the notification. 86 | func (s *Hipchat) Handle(e event.Event) { 87 | client := hipchat.NewClient(s.Token) 88 | if s.Url != "" { 89 | baseUrl, err := url.Parse(s.Url) 90 | if err != nil { 91 | panic(err) 92 | } 93 | client.BaseURL = baseUrl 94 | } 95 | 96 | notificationRequest := prepareHipchatNotification(e) 97 | _, err := client.Room.Notification(s.Room, ¬ificationRequest) 98 | 99 | if err != nil { 100 | log.Printf("%s\n", err) 101 | return 102 | } 103 | 104 | log.Printf("Message successfully sent to room %s", s.Room) 105 | } 106 | 107 | func checkMissingHipchatVars(s *Hipchat) error { 108 | if s.Token == "" || s.Room == "" { 109 | return fmt.Errorf(hipchatErrMsg, "Missing hipchat token or room") 110 | } 111 | 112 | return nil 113 | } 114 | 115 | func prepareHipchatNotification(e event.Event) hipchat.NotificationRequest { 116 | notification := hipchat.NotificationRequest{ 117 | Message: e.Message(), 118 | Notify: true, 119 | From: "kubewatch", 120 | } 121 | 122 | if color, ok := hipchatColors[e.Status]; ok { 123 | 124 | notification.Color = color 125 | } 126 | 127 | return notification 128 | } 129 | -------------------------------------------------------------------------------- /pkg/handlers/hipchat/hipchat_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package hipchat 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | "github.com/bitnami-labs/kubewatch/config" 25 | ) 26 | 27 | func TestHipchatInit(t *testing.T) { 28 | s := &Hipchat{} 29 | expectedError := fmt.Errorf(hipchatErrMsg, "Missing hipchat token or room") 30 | 31 | var Tests = []struct { 32 | hipchat config.Hipchat 33 | err error 34 | }{ 35 | {config.Hipchat{Token: "foo", Room: "bar"}, nil}, 36 | {config.Hipchat{Token: "foo"}, expectedError}, 37 | {config.Hipchat{Room: "bar"}, expectedError}, 38 | {config.Hipchat{}, expectedError}, 39 | } 40 | 41 | for _, tt := range Tests { 42 | c := &config.Config{} 43 | c.Handler.Hipchat = tt.hipchat 44 | if err := s.Init(c); !reflect.DeepEqual(err, tt.err) { 45 | t.Fatalf("Init(): %v", err) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/handlers/mattermost/mattermost.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mattermost 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "os" 23 | 24 | "bytes" 25 | "encoding/json" 26 | "net/http" 27 | "time" 28 | 29 | "github.com/bitnami-labs/kubewatch/config" 30 | "github.com/bitnami-labs/kubewatch/pkg/event" 31 | ) 32 | 33 | var mattermostColors = map[string]string{ 34 | "Normal": "#00FF00", 35 | "Warning": "#FFFF00", 36 | "Danger": "#FF0000", 37 | } 38 | 39 | var mattermostErrMsg = ` 40 | %s 41 | 42 | You need to set Mattermost url, channel and username for Mattermost notify, 43 | using "--channel/-c", "--url/-u" and "--username/-n", or using environment variables: 44 | 45 | export KW_MATTERMOST_CHANNEL=mattermost_channel 46 | export KW_MATTERMOST_URL=mattermost_url 47 | export KW_MATTERMOST_USERNAME=mattermost_username 48 | 49 | Command line flags will override environment variables 50 | 51 | ` 52 | 53 | // Mattermost handler implements handler.Handler interface, 54 | // Notify event to Mattermost channel 55 | type Mattermost struct { 56 | Channel string 57 | Url string 58 | Username string 59 | } 60 | 61 | // MattermostMessage struct for messages 62 | type MattermostMessage struct { 63 | Channel string `json:"channel"` 64 | Username string `json:"username"` 65 | IconUrl string `json:"icon_url"` 66 | Text string `json:"text"` 67 | Attachements []MattermostMessageAttachement `json:"attachments"` 68 | } 69 | 70 | // MattermostMessageAttachement for message attachments 71 | type MattermostMessageAttachement struct { 72 | Title string `json:"title"` 73 | Color string `json:"color"` 74 | } 75 | 76 | // Init prepares Mattermost configuration 77 | func (m *Mattermost) Init(c *config.Config) error { 78 | channel := c.Handler.Mattermost.Channel 79 | url := c.Handler.Mattermost.Url 80 | username := c.Handler.Mattermost.Username 81 | 82 | if channel == "" { 83 | channel = os.Getenv("KW_MATTERMOST_CHANNEL") 84 | } 85 | 86 | if url == "" { 87 | url = os.Getenv("KW_MATTERMOST_URL") 88 | } 89 | 90 | if username == "" { 91 | username = os.Getenv("KW_MATTERMOST_USERNAME") 92 | } 93 | 94 | m.Channel = channel 95 | m.Url = url 96 | m.Username = username 97 | 98 | return checkMissingMattermostVars(m) 99 | } 100 | 101 | // Handle handles an event. 102 | func (m *Mattermost) Handle(e event.Event) { 103 | mattermostMessage := prepareMattermostMessage(e, m) 104 | 105 | err := postMessage(m.Url, mattermostMessage) 106 | if err != nil { 107 | log.Printf("%s\n", err) 108 | return 109 | } 110 | 111 | log.Printf("Message successfully sent to channel %s at %s", m.Channel, time.Now()) 112 | } 113 | 114 | func checkMissingMattermostVars(s *Mattermost) error { 115 | if s.Channel == "" || s.Url == "" || s.Username == "" { 116 | return fmt.Errorf(mattermostErrMsg, "Missing Mattermost channel, url or username") 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func prepareMattermostMessage(e event.Event, m *Mattermost) *MattermostMessage { 123 | return &MattermostMessage{ 124 | Channel: m.Channel, 125 | Username: m.Username, 126 | IconUrl: "https://raw.githubusercontent.com/kubernetes/kubernetes/master/logo/logo_with_border.png", 127 | Attachements: []MattermostMessageAttachement{ 128 | { 129 | Title: e.Message(), 130 | Color: mattermostColors[e.Status], 131 | }, 132 | }, 133 | } 134 | } 135 | 136 | func postMessage(url string, mattermostMessage *MattermostMessage) error { 137 | message, err := json.Marshal(mattermostMessage) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(message)) 143 | if err != nil { 144 | return err 145 | } 146 | req.Header.Add("Content-Type", "application/json") 147 | 148 | client := &http.Client{} 149 | _, err = client.Do(req) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /pkg/handlers/mattermost/mattermost_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mattermost 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | "github.com/bitnami-labs/kubewatch/config" 25 | ) 26 | 27 | func TestMattermostInit(t *testing.T) { 28 | s := &Mattermost{} 29 | expectedError := fmt.Errorf(mattermostErrMsg, "Missing Mattermost channel, url or username") 30 | 31 | var Tests = []struct { 32 | mattermost config.Mattermost 33 | err error 34 | }{ 35 | {config.Mattermost{Url: "foo", Channel: "bar", Username: "username"}, nil}, 36 | {config.Mattermost{Url: "foo", Channel: "bar"}, expectedError}, 37 | {config.Mattermost{Url: "foo", Username: "username"}, expectedError}, 38 | {config.Mattermost{Channel: "foo", Username: "username"}, expectedError}, 39 | {config.Mattermost{Url: "foo"}, expectedError}, 40 | {config.Mattermost{Channel: "bar"}, expectedError}, 41 | {config.Mattermost{Username: "bar"}, expectedError}, 42 | {config.Mattermost{}, expectedError}, 43 | } 44 | 45 | for _, tt := range Tests { 46 | c := &config.Config{} 47 | c.Handler.Mattermost = tt.mattermost 48 | if err := s.Init(c); !reflect.DeepEqual(err, tt.err) { 49 | t.Fatalf("Init(): %v", err) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/handlers/msteam/msteam.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package msteam 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "io/ioutil" 24 | "log" 25 | "net/http" 26 | "os" 27 | 28 | "github.com/bitnami-labs/kubewatch/config" 29 | "github.com/bitnami-labs/kubewatch/pkg/event" 30 | ) 31 | 32 | var msteamsErrMsg = ` 33 | %s 34 | 35 | You need to set the MS teams webhook URL, 36 | using --webhookURL, or using environment variables: 37 | 38 | export KW_MSTEAMS_WEBHOOKURL=webhook_url 39 | 40 | Command line flags will override environment variables 41 | 42 | ` 43 | 44 | var msTeamsColors = map[string]string{ 45 | "Normal": "2DC72D", 46 | "Warning": "DEFF22", 47 | "Danger": "8C1A1A", 48 | } 49 | 50 | // Constants for Sending a Card 51 | const ( 52 | messageType = "MessageCard" 53 | context = "http://schema.org/extensions" 54 | ) 55 | 56 | // TeamsMessageCard is for the Card Fields to send in Teams 57 | // The Documentation is in https://docs.microsoft.com/en-us/outlook/actionable-messages/card-reference#card-fields 58 | type TeamsMessageCard struct { 59 | Type string `json:"@type"` 60 | Context string `json:"@context"` 61 | ThemeColor string `json:"themeColor"` 62 | Summary string `json:"summary"` 63 | Title string `json:"title"` 64 | Text string `json:"text,omitempty"` 65 | Sections []TeamsMessageCardSection `json:"sections"` 66 | } 67 | 68 | // TeamsMessageCardSection is placed under TeamsMessageCard.Sections 69 | // Each element of AlertWebHook.Alerts will the number of elements of TeamsMessageCard.Sections to create 70 | type TeamsMessageCardSection struct { 71 | ActivityTitle string `json:"activityTitle"` 72 | Facts []TeamsMessageCardSectionFacts `json:"facts"` 73 | Markdown bool `json:"markdown"` 74 | } 75 | 76 | // TeamsMessageCardSectionFacts is placed under TeamsMessageCardSection.Facts 77 | type TeamsMessageCardSectionFacts struct { 78 | Name string `json:"name"` 79 | Value string `json:"value"` 80 | } 81 | 82 | // Default handler implements Handler interface, 83 | // print each event with JSON format 84 | type MSTeams struct { 85 | // TeamsWebhookURL is the webhook url of the Teams connector 86 | TeamsWebhookURL string 87 | } 88 | 89 | // sendCard sends the JSON Encoded TeamsMessageCard to the webhook URL 90 | func sendCard(ms *MSTeams, card *TeamsMessageCard) (*http.Response, error) { 91 | buffer := new(bytes.Buffer) 92 | if err := json.NewEncoder(buffer).Encode(card); err != nil { 93 | return nil, fmt.Errorf("Failed encoding message card: %v", err) 94 | } 95 | res, err := http.Post(ms.TeamsWebhookURL, "application/json", buffer) 96 | if err != nil { 97 | return nil, fmt.Errorf("Failed sending to webhook url %s. Got the error: %v", 98 | ms.TeamsWebhookURL, err) 99 | } 100 | if res.StatusCode != http.StatusOK { 101 | resMessage, err := ioutil.ReadAll(res.Body) 102 | if err != nil { 103 | return nil, fmt.Errorf("Failed reading Teams http response: %v", err) 104 | } 105 | return nil, fmt.Errorf("Failed sending to the Teams Channel. Teams http response: %s, %s", 106 | res.Status, string(resMessage)) 107 | } 108 | if err := res.Body.Close(); err != nil { 109 | return nil, err 110 | } 111 | return res, nil 112 | } 113 | 114 | // Init initializes handler configuration 115 | func (ms *MSTeams) Init(c *config.Config) error { 116 | webhookURL := c.Handler.MSTeams.WebhookURL 117 | 118 | if webhookURL == "" { 119 | webhookURL = os.Getenv("KW_MSTEAMS_WEBHOOKURL") 120 | } 121 | 122 | if webhookURL == "" { 123 | return fmt.Errorf(msteamsErrMsg, "Missing MS teams webhook URL") 124 | } 125 | 126 | ms.TeamsWebhookURL = webhookURL 127 | return nil 128 | } 129 | 130 | // Handle handles notification. 131 | func (ms *MSTeams) Handle(e event.Event) { 132 | card := &TeamsMessageCard{ 133 | Type: messageType, 134 | Context: context, 135 | Title: "kubewatch", 136 | // Set a default Summary, this is required for Microsoft Teams 137 | Summary: "kubewatch notification received", 138 | } 139 | 140 | card.ThemeColor = msTeamsColors[e.Status] 141 | 142 | var s TeamsMessageCardSection 143 | s.ActivityTitle = e.Message() 144 | s.Markdown = true 145 | card.Sections = append(card.Sections, s) 146 | 147 | if _, err := sendCard(ms, card); err != nil { 148 | log.Printf("%s\n", err) 149 | return 150 | } 151 | 152 | log.Printf("Message successfully sent to MS Teams") 153 | } 154 | -------------------------------------------------------------------------------- /pkg/handlers/msteam/msteam_test.go: -------------------------------------------------------------------------------- 1 | package msteam 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/bitnami-labs/kubewatch/config" 12 | "github.com/bitnami-labs/kubewatch/pkg/event" 13 | ) 14 | 15 | // Tests the Init() function 16 | func TestInit(t *testing.T) { 17 | s := &MSTeams{} 18 | expectedError := fmt.Errorf(msteamsErrMsg, "Missing MS teams webhook URL") 19 | 20 | var Tests = []struct { 21 | ms config.MSTeams 22 | err error 23 | }{ 24 | {config.MSTeams{WebhookURL: "somepath"}, nil}, 25 | {config.MSTeams{}, expectedError}, 26 | } 27 | 28 | for _, tt := range Tests { 29 | c := &config.Config{} 30 | c.Handler.MSTeams = tt.ms 31 | if err := s.Init(c); !reflect.DeepEqual(err, tt.err) { 32 | t.Fatalf("Init(): %v", err) 33 | } 34 | } 35 | } 36 | 37 | // Tests ObjectCreated() by passing v1.Pod 38 | func TestObjectCreated(t *testing.T) { 39 | expectedCard := TeamsMessageCard{ 40 | Type: messageType, 41 | Context: context, 42 | ThemeColor: msTeamsColors["Normal"], 43 | Summary: "kubewatch notification received", 44 | Title: "kubewatch", 45 | Text: "", 46 | Sections: []TeamsMessageCardSection{ 47 | { 48 | ActivityTitle: "A `pod` in namespace `new` has been `Created`:\n`foo`", 49 | Markdown: true, 50 | }, 51 | }, 52 | } 53 | 54 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | w.WriteHeader(http.StatusOK) 56 | if r.Method != "POST" { 57 | t.Errorf("expected a POST request for ObjectCreated()") 58 | } 59 | decoder := json.NewDecoder(r.Body) 60 | var c TeamsMessageCard 61 | if err := decoder.Decode(&c); err != nil { 62 | t.Errorf("%v", err) 63 | } 64 | if !reflect.DeepEqual(c, expectedCard) { 65 | t.Errorf("expected %v, got %v", expectedCard, c) 66 | } 67 | })) 68 | 69 | ms := &MSTeams{TeamsWebhookURL: ts.URL} 70 | p := event.Event{ 71 | Name: "foo", 72 | Kind: "pod", 73 | Namespace: "new", 74 | Reason: "Created", 75 | Status: "Normal", 76 | } 77 | 78 | ms.Handle(p) 79 | } 80 | 81 | // Tests ObjectDeleted() by passing v1.Pod 82 | func TestObjectDeleted(t *testing.T) { 83 | expectedCard := TeamsMessageCard{ 84 | Type: messageType, 85 | Context: context, 86 | ThemeColor: msTeamsColors["Danger"], 87 | Summary: "kubewatch notification received", 88 | Title: "kubewatch", 89 | Text: "", 90 | Sections: []TeamsMessageCardSection{ 91 | { 92 | ActivityTitle: "A `pod` in namespace `new` has been `Deleted`:\n`foo`", 93 | Markdown: true, 94 | }, 95 | }, 96 | } 97 | 98 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 99 | w.WriteHeader(http.StatusOK) 100 | if r.Method != "POST" { 101 | t.Errorf("expected a POST request for ObjectDeleted()") 102 | } 103 | decoder := json.NewDecoder(r.Body) 104 | var c TeamsMessageCard 105 | if err := decoder.Decode(&c); err != nil { 106 | t.Errorf("%v", err) 107 | } 108 | if !reflect.DeepEqual(c, expectedCard) { 109 | t.Errorf("expected %v, got %v", expectedCard, c) 110 | } 111 | })) 112 | 113 | ms := &MSTeams{TeamsWebhookURL: ts.URL} 114 | 115 | p := event.Event{ 116 | Name: "foo", 117 | Namespace: "new", 118 | Kind: "pod", 119 | Reason: "Deleted", 120 | Status: "Danger", 121 | } 122 | 123 | ms.Handle(p) 124 | } 125 | 126 | // Tests ObjectUpdated() by passing v1.Pod 127 | func TestObjectUpdated(t *testing.T) { 128 | expectedCard := TeamsMessageCard{ 129 | Type: messageType, 130 | Context: context, 131 | ThemeColor: msTeamsColors["Warning"], 132 | Summary: "kubewatch notification received", 133 | Title: "kubewatch", 134 | Text: "", 135 | Sections: []TeamsMessageCardSection{ 136 | { 137 | ActivityTitle: "A `pod` in namespace `new` has been `Updated`:\n`foo`", 138 | Markdown: true, 139 | }, 140 | }, 141 | } 142 | 143 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 | w.WriteHeader(http.StatusOK) 145 | if r.Method != "POST" { 146 | t.Errorf("expected a POST request for ObjectUpdated()") 147 | } 148 | decoder := json.NewDecoder(r.Body) 149 | var c TeamsMessageCard 150 | if err := decoder.Decode(&c); err != nil { 151 | t.Errorf("%v", err) 152 | } 153 | if !reflect.DeepEqual(c, expectedCard) { 154 | t.Errorf("expected %v, got %v", expectedCard, c) 155 | } 156 | })) 157 | 158 | ms := &MSTeams{TeamsWebhookURL: ts.URL} 159 | 160 | oldP := event.Event{ 161 | Name: "foo", 162 | Namespace: "new", 163 | Kind: "pod", 164 | Reason: "Updated", 165 | Status: "Warning", 166 | } 167 | 168 | newP := event.Event{ 169 | Name: "foo-new", 170 | Namespace: "new", 171 | Kind: "pod", 172 | Reason: "Updated", 173 | Status: "Warning", 174 | } 175 | _ = newP 176 | 177 | ms.Handle(oldP) 178 | } 179 | -------------------------------------------------------------------------------- /pkg/handlers/slack/slack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package slack 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "os" 23 | 24 | "github.com/slack-go/slack" 25 | 26 | "github.com/bitnami-labs/kubewatch/config" 27 | "github.com/bitnami-labs/kubewatch/pkg/event" 28 | ) 29 | 30 | var slackColors = map[string]string{ 31 | "Normal": "good", 32 | "Warning": "warning", 33 | "Danger": "danger", 34 | } 35 | 36 | var slackErrMsg = ` 37 | %s 38 | 39 | You need to set both slack token and channel for slack notify, 40 | using "--token/-t" and "--channel/-c", or using environment variables: 41 | 42 | export KW_SLACK_TOKEN=slack_token 43 | export KW_SLACK_CHANNEL=slack_channel 44 | 45 | Command line flags will override environment variables 46 | 47 | ` 48 | 49 | // Slack handler implements handler.Handler interface, 50 | // Notify event to slack channel 51 | type Slack struct { 52 | Token string 53 | Channel string 54 | Title string 55 | } 56 | 57 | // Init prepares slack configuration 58 | func (s *Slack) Init(c *config.Config) error { 59 | token := c.Handler.Slack.Token 60 | channel := c.Handler.Slack.Channel 61 | title := c.Handler.Slack.Title 62 | 63 | if token == "" { 64 | token = os.Getenv("KW_SLACK_TOKEN") 65 | } 66 | 67 | if channel == "" { 68 | channel = os.Getenv("KW_SLACK_CHANNEL") 69 | } 70 | 71 | if title == "" { 72 | title = os.Getenv("KW_SLACK_TITLE") 73 | if title == "" { 74 | title = "kubewatch" 75 | } 76 | } 77 | 78 | s.Token = token 79 | s.Channel = channel 80 | s.Title = title 81 | 82 | return checkMissingSlackVars(s) 83 | } 84 | 85 | // Handle handles the notification. 86 | func (s *Slack) Handle(e event.Event) { 87 | api := slack.New(s.Token) 88 | attachment := prepareSlackAttachment(e, s) 89 | 90 | channelID, timestamp, err := api.PostMessage(s.Channel, 91 | slack.MsgOptionAttachments(attachment), 92 | slack.MsgOptionAsUser(true)) 93 | if err != nil { 94 | log.Printf("%s\n", err) 95 | return 96 | } 97 | 98 | log.Printf("Message successfully sent to channel %s at %s", channelID, timestamp) 99 | } 100 | 101 | func checkMissingSlackVars(s *Slack) error { 102 | if s.Token == "" || s.Channel == "" { 103 | return fmt.Errorf(slackErrMsg, "Missing slack token or channel") 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func prepareSlackAttachment(e event.Event, s *Slack) slack.Attachment { 110 | 111 | attachment := slack.Attachment{ 112 | Fields: []slack.AttachmentField{ 113 | { 114 | Title: s.Title, 115 | Value: e.Message(), 116 | }, 117 | }, 118 | } 119 | 120 | if color, ok := slackColors[e.Status]; ok { 121 | attachment.Color = color 122 | } 123 | 124 | attachment.MarkdownIn = []string{"fields"} 125 | 126 | return attachment 127 | } 128 | -------------------------------------------------------------------------------- /pkg/handlers/slack/slack_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Skippbox, Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package slack 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | "github.com/bitnami-labs/kubewatch/config" 25 | ) 26 | 27 | func TestSlackInit(t *testing.T) { 28 | s := &Slack{} 29 | expectedError := fmt.Errorf(slackErrMsg, "Missing slack token or channel") 30 | 31 | var Tests = []struct { 32 | slack config.Slack 33 | err error 34 | }{ 35 | {config.Slack{Token: "foo", Channel: "bar"}, nil}, 36 | {config.Slack{Token: "foo"}, expectedError}, 37 | {config.Slack{Channel: "bar"}, expectedError}, 38 | {config.Slack{}, expectedError}, 39 | } 40 | 41 | for _, tt := range Tests { 42 | c := &config.Config{} 43 | c.Handler.Slack = tt.slack 44 | if err := s.Init(c); !reflect.DeepEqual(err, tt.err) { 45 | t.Fatalf("Init(): %v", err) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/handlers/smtp/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 VMWare 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | This code is adapted from https://github.com/prometheus/alertmanager/blob/a75cd02786dfecd25e2469fc4df5d920e6b9c226/notify/email/email.go 19 | */ 20 | 21 | package smtp 22 | 23 | import ( 24 | "bytes" 25 | "context" 26 | "crypto/tls" 27 | "fmt" 28 | "log" 29 | "math/rand" 30 | "mime" 31 | "mime/multipart" 32 | "mime/quotedprintable" 33 | "net" 34 | "net/mail" 35 | "net/smtp" 36 | "net/textproto" 37 | "os" 38 | "strings" 39 | "time" 40 | 41 | "github.com/bitnami-labs/kubewatch/config" 42 | "github.com/mkmik/multierror" 43 | "github.com/sirupsen/logrus" 44 | ) 45 | 46 | func sendEmail(conf config.SMTP, msg string) error { 47 | ctx := context.Background() 48 | 49 | host, port, err := net.SplitHostPort(conf.Smarthost) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | var ( 55 | c *smtp.Client 56 | conn net.Conn 57 | success = false 58 | ) 59 | 60 | tlsConfig := &tls.Config{} 61 | if port == "465" { 62 | 63 | if tlsConfig.ServerName == "" { 64 | tlsConfig.ServerName = host 65 | } 66 | 67 | conn, err = tls.Dial("tcp", conf.Smarthost, tlsConfig) 68 | if err != nil { 69 | return fmt.Errorf("establish TLS connection to server: %w", err) 70 | } 71 | } else { 72 | var ( 73 | d = net.Dialer{} 74 | err error 75 | ) 76 | conn, err = d.DialContext(ctx, "tcp", conf.Smarthost) 77 | if err != nil { 78 | return fmt.Errorf("establish connection to server: %w", err) 79 | } 80 | } 81 | c, err = smtp.NewClient(conn, host) 82 | if err != nil { 83 | conn.Close() 84 | return fmt.Errorf("create SMTP client: %w", err) 85 | } 86 | defer func() { 87 | // Try to clean up after ourselves but don't log anything if something has failed. 88 | if err := c.Quit(); success && err != nil { 89 | logrus.Warnf("failed to close SMTP connection: %v", err) 90 | } 91 | }() 92 | 93 | if conf.Hello != "" { 94 | err = c.Hello(conf.Hello) 95 | if err != nil { 96 | return fmt.Errorf("send EHLO command: %w", err) 97 | } 98 | } 99 | 100 | // Global Config guarantees RequireTLS is not nil. 101 | if conf.RequireTLS { 102 | if ok, _ := c.Extension("STARTTLS"); !ok { 103 | return fmt.Errorf("'require_tls' is true (default) but %q does not advertise the STARTTLS extension", conf.Smarthost) 104 | } 105 | if tlsConfig.ServerName == "" { 106 | tlsConfig.ServerName = host 107 | } 108 | 109 | if err := c.StartTLS(tlsConfig); err != nil { 110 | return fmt.Errorf("send STARTTLS command: %w", err) 111 | } 112 | } 113 | 114 | if ok, mech := c.Extension("AUTH"); ok { 115 | auth, err := auth(conf.Auth, host, mech) 116 | if err != nil { 117 | return fmt.Errorf("find auth mechanism: %w", err) 118 | } 119 | if auth != nil { 120 | if err := c.Auth(auth); err != nil { 121 | return fmt.Errorf("%T auth: %w", auth, err) 122 | } 123 | } 124 | } 125 | 126 | addrs, err := mail.ParseAddressList(conf.From) 127 | if err != nil { 128 | return fmt.Errorf("parse 'from' addresses: %w", err) 129 | } 130 | if len(addrs) != 1 { 131 | return fmt.Errorf("must be exactly one 'from' address (got: %d)", len(addrs)) 132 | } 133 | if err = c.Mail(addrs[0].Address); err != nil { 134 | return fmt.Errorf("send MAIL command: %w", err) 135 | } 136 | addrs, err = mail.ParseAddressList(conf.To) 137 | if err != nil { 138 | return fmt.Errorf("parse 'to' addresses: %w", err) 139 | } 140 | for _, addr := range addrs { 141 | if err = c.Rcpt(addr.Address); err != nil { 142 | return fmt.Errorf("send RCPT command: %w", err) 143 | } 144 | } 145 | 146 | // Send the email headers and body. 147 | message, err := c.Data() 148 | if err != nil { 149 | return fmt.Errorf("send DATA command: %w", err) 150 | } 151 | defer message.Close() 152 | 153 | if conf.Headers == nil { 154 | conf.Headers = map[string]string{} 155 | } 156 | if _, ok := conf.Headers["Subject"]; !ok { 157 | s := conf.Subject 158 | if s == "" { 159 | s = defaultSubject 160 | } 161 | conf.Headers["Subject"] = s 162 | } 163 | if _, ok := conf.Headers["To"]; !ok { 164 | conf.Headers["To"] = conf.To 165 | } 166 | if _, ok := conf.Headers["From"]; !ok { 167 | conf.Headers["From"] = conf.From 168 | } 169 | 170 | buffer := &bytes.Buffer{} 171 | for header, value := range conf.Headers { 172 | fmt.Fprintf(buffer, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value)) 173 | } 174 | 175 | hostname, err := os.Hostname() 176 | if err != nil { 177 | return err 178 | } 179 | if _, ok := conf.Headers["Message-Id"]; !ok { 180 | fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), hostname)) 181 | } 182 | 183 | multipartBuffer := &bytes.Buffer{} 184 | multipartWriter := multipart.NewWriter(multipartBuffer) 185 | 186 | fmt.Fprintf(buffer, "Date: %s\r\n", time.Now().Format(time.RFC1123Z)) 187 | fmt.Fprintf(buffer, "Content-Type: multipart/alternative; boundary=%s\r\n", multipartWriter.Boundary()) 188 | fmt.Fprintf(buffer, "MIME-Version: 1.0\r\n\r\n") 189 | 190 | _, err = message.Write(buffer.Bytes()) 191 | if err != nil { 192 | return fmt.Errorf("write headers: %w", err) 193 | } 194 | w, err := multipartWriter.CreatePart(textproto.MIMEHeader{ 195 | "Content-Transfer-Encoding": {"quoted-printable"}, 196 | "Content-Type": {"text/plain; charset=UTF-8"}, 197 | }) 198 | if err != nil { 199 | return fmt.Errorf("create part for text template: %w", err) 200 | } 201 | 202 | qw := quotedprintable.NewWriter(w) 203 | _, err = qw.Write([]byte(msg)) 204 | if err != nil { 205 | return fmt.Errorf("write text part: %w", err) 206 | } 207 | err = qw.Close() 208 | if err != nil { 209 | return fmt.Errorf("close text part: %w", err) 210 | } 211 | 212 | err = multipartWriter.Close() 213 | if err != nil { 214 | return fmt.Errorf("close multipartWriter: %w", err) 215 | 216 | } 217 | 218 | _, err = message.Write(multipartBuffer.Bytes()) 219 | if err != nil { 220 | return fmt.Errorf("write body buffer: %w", err) 221 | } 222 | 223 | log.Printf("sending via %s:%s, to: %q, from: %q : %s ", host, port, conf.To, conf.From, msg) 224 | return nil 225 | } 226 | 227 | func auth(conf config.SMTPAuth, host, mechs string) (smtp.Auth, error) { 228 | username := conf.Username 229 | 230 | // If no username is set, keep going without authentication. 231 | if username == "" { 232 | logrus.Debugf("smtp_auth_username is not configured. Attempting to send email without authenticating") 233 | return nil, nil 234 | } 235 | 236 | var errs []error 237 | for _, mech := range strings.Split(mechs, " ") { 238 | switch mech { 239 | case "CRAM-MD5": 240 | secret := string(conf.Secret) 241 | if secret == "" { 242 | errs = append(errs, fmt.Errorf("missing secret for CRAM-MD5 auth mechanism")) 243 | continue 244 | } 245 | return smtp.CRAMMD5Auth(username, secret), nil 246 | 247 | case "PLAIN": 248 | password := string(conf.Password) 249 | if password == "" { 250 | errs = append(errs, fmt.Errorf("missing password for PLAIN auth mechanism")) 251 | continue 252 | } 253 | identity := conf.Identity 254 | 255 | return smtp.PlainAuth(identity, username, password, host), nil 256 | case "LOGIN": 257 | password := string(conf.Password) 258 | if password == "" { 259 | errs = append(errs, fmt.Errorf("missing password for LOGIN auth mechanism")) 260 | continue 261 | } 262 | return LoginAuth(username, password), nil 263 | } 264 | } 265 | if len(errs) == 0 { 266 | errs = append(errs, fmt.Errorf("unknown auth mechanism: %q", mechs)) 267 | } 268 | return nil, multierror.Join(errs) 269 | } 270 | 271 | type loginAuth struct { 272 | username, password string 273 | } 274 | 275 | func LoginAuth(username, password string) smtp.Auth { 276 | return &loginAuth{username, password} 277 | } 278 | 279 | func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { 280 | return "LOGIN", []byte{}, nil 281 | } 282 | 283 | // Used for AUTH LOGIN. (Maybe password should be encrypted) 284 | func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { 285 | if more { 286 | switch strings.ToLower(string(fromServer)) { 287 | case "username:": 288 | return []byte(a.username), nil 289 | case "password:": 290 | return []byte(a.password), nil 291 | default: 292 | return nil, fmt.Errorf("unexpected server challenge") 293 | } 294 | } 295 | return nil, nil 296 | } 297 | -------------------------------------------------------------------------------- /pkg/handlers/smtp/smtp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 VMWare 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Package smtp implements an email notification handler for kubewatch. 19 | 20 | See example configuration in the ConfigExample constant. 21 | */ 22 | package smtp 23 | 24 | import ( 25 | "fmt" 26 | "log" 27 | "time" 28 | 29 | "github.com/bitnami-labs/kubewatch/config" 30 | "github.com/bitnami-labs/kubewatch/pkg/event" 31 | "github.com/sirupsen/logrus" 32 | ) 33 | 34 | const ( 35 | defaultSubject = "Kubewatch notification" 36 | 37 | // ConfigExample is an example configuration. 38 | ConfigExample = `handler: 39 | smtp: 40 | to: "myteam@mycompany.com" 41 | from: "kubewatch@mycluster.com" 42 | smarthost: smtp.mycompany.com:2525 43 | subject: Test notification 44 | auth: 45 | username: myusername 46 | password: mypassword 47 | requireTLS: true 48 | ` 49 | ) 50 | 51 | // SMTP handler implements handler.Handler interface, 52 | // Notify event via email. 53 | type SMTP struct { 54 | cfg config.SMTP 55 | } 56 | 57 | // Init prepares Webhook configuration 58 | func (s *SMTP) Init(c *config.Config) error { 59 | s.cfg = c.Handler.SMTP 60 | 61 | if s.cfg.To == "" { 62 | return fmt.Errorf("smtp `to` conf field is required") 63 | } 64 | if s.cfg.From == "" { 65 | return fmt.Errorf("smtp `from` conf field is required") 66 | } 67 | if s.cfg.Smarthost == "" { 68 | return fmt.Errorf("smtp `smarthost` conf field is required") 69 | } 70 | return nil 71 | } 72 | 73 | // Handle handles the notification. 74 | func (s *SMTP) Handle(e event.Event) { 75 | send(s.cfg, e.Message()) 76 | log.Printf("Message successfully sent to %s at %s ", s.cfg.To, time.Now()) 77 | } 78 | 79 | func formatEmail(e event.Event) (string, error) { 80 | return e.Message(), nil 81 | } 82 | 83 | func send(conf config.SMTP, msg string) { 84 | if err := sendEmail(conf, msg); err != nil { 85 | logrus.Error(err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/handlers/smtp/smtp_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 VMWare 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package smtp 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestSMTP(t *testing.T) { 24 | // TODO(mkmik): setup a in-memory smtp server like https://github.com/bradfitz/go-smtpd 25 | } 26 | -------------------------------------------------------------------------------- /pkg/handlers/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Bitnami 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webhook 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "os" 23 | 24 | "bytes" 25 | "encoding/json" 26 | "net/http" 27 | "time" 28 | 29 | "github.com/bitnami-labs/kubewatch/config" 30 | "github.com/bitnami-labs/kubewatch/pkg/event" 31 | ) 32 | 33 | var webhookErrMsg = ` 34 | %s 35 | 36 | You need to set Webhook url 37 | using "--url/-u" or using environment variables: 38 | 39 | export KW_WEBHOOK_URL=webhook_url 40 | 41 | Command line flags will override environment variables 42 | 43 | ` 44 | 45 | // Webhook handler implements handler.Handler interface, 46 | // Notify event to Webhook channel 47 | type Webhook struct { 48 | Url string 49 | } 50 | 51 | // WebhookMessage for messages 52 | type WebhookMessage struct { 53 | EventMeta EventMeta `json:"eventmeta"` 54 | Text string `json:"text"` 55 | Time time.Time `json:"time"` 56 | } 57 | 58 | // EventMeta containes the meta data about the event occurred 59 | type EventMeta struct { 60 | Kind string `json:"kind"` 61 | Name string `json:"name"` 62 | Namespace string `json:"namespace"` 63 | Reason string `json:"reason"` 64 | } 65 | 66 | // Init prepares Webhook configuration 67 | func (m *Webhook) Init(c *config.Config) error { 68 | url := c.Handler.Webhook.Url 69 | 70 | if url == "" { 71 | url = os.Getenv("KW_WEBHOOK_URL") 72 | } 73 | 74 | m.Url = url 75 | 76 | return checkMissingWebhookVars(m) 77 | } 78 | 79 | // Handle handles an event. 80 | func (m *Webhook) Handle(e event.Event) { 81 | webhookMessage := prepareWebhookMessage(e, m) 82 | 83 | err := postMessage(m.Url, webhookMessage) 84 | if err != nil { 85 | log.Printf("%s\n", err) 86 | return 87 | } 88 | 89 | log.Printf("Message successfully sent to %s at %s ", m.Url, time.Now()) 90 | } 91 | 92 | func checkMissingWebhookVars(s *Webhook) error { 93 | if s.Url == "" { 94 | return fmt.Errorf(webhookErrMsg, "Missing Webhook url") 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func prepareWebhookMessage(e event.Event, m *Webhook) *WebhookMessage { 101 | return &WebhookMessage{ 102 | EventMeta: EventMeta{ 103 | Kind: e.Kind, 104 | Name: e.Name, 105 | Namespace: e.Namespace, 106 | Reason: e.Reason, 107 | }, 108 | Text: e.Message(), 109 | Time: time.Now(), 110 | } 111 | } 112 | 113 | func postMessage(url string, webhookMessage *WebhookMessage) error { 114 | message, err := json.Marshal(webhookMessage) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(message)) 120 | if err != nil { 121 | return err 122 | } 123 | req.Header.Add("Content-Type", "application/json") 124 | 125 | client := &http.Client{} 126 | _, err = client.Do(req) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /pkg/handlers/webhook/webhook_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Bitnami 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webhook 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | "github.com/bitnami-labs/kubewatch/config" 25 | ) 26 | 27 | func TestWebhookInit(t *testing.T) { 28 | s := &Webhook{} 29 | expectedError := fmt.Errorf(webhookErrMsg, "Missing Webhook url") 30 | 31 | var Tests = []struct { 32 | webhook config.Webhook 33 | err error 34 | }{ 35 | {config.Webhook{Url: "foo"}, nil}, 36 | {config.Webhook{}, expectedError}, 37 | } 38 | 39 | for _, tt := range Tests { 40 | c := &config.Config{} 41 | c.Handler.Webhook = tt.webhook 42 | if err := s.Init(c); !reflect.DeepEqual(err, tt.err) { 43 | t.Fatalf("Init(): %v", err) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/utils/k8sutil.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | apps_v1 "k8s.io/api/apps/v1" 8 | batch_v1 "k8s.io/api/batch/v1" 9 | api_v1 "k8s.io/api/core/v1" 10 | ext_v1beta1 "k8s.io/api/extensions/v1beta1" 11 | rbac_v1beta1 "k8s.io/api/rbac/v1beta1" 12 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/client-go/kubernetes" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/client-go/tools/clientcmd" 16 | ) 17 | 18 | // GetClient returns a k8s clientset to the request from inside of cluster 19 | func GetClient() kubernetes.Interface { 20 | config, err := rest.InClusterConfig() 21 | if err != nil { 22 | logrus.Fatalf("Can not get kubernetes config: %v", err) 23 | } 24 | 25 | clientset, err := kubernetes.NewForConfig(config) 26 | if err != nil { 27 | logrus.Fatalf("Can not create kubernetes client: %v", err) 28 | } 29 | 30 | return clientset 31 | } 32 | 33 | func buildOutOfClusterConfig() (*rest.Config, error) { 34 | kubeconfigPath := os.Getenv("KUBECONFIG") 35 | if kubeconfigPath == "" { 36 | kubeconfigPath = os.Getenv("HOME") + "/.kube/config" 37 | } 38 | return clientcmd.BuildConfigFromFlags("", kubeconfigPath) 39 | } 40 | 41 | // GetClientOutOfCluster returns a k8s clientset to the request from outside of cluster 42 | func GetClientOutOfCluster() kubernetes.Interface { 43 | config, err := buildOutOfClusterConfig() 44 | if err != nil { 45 | logrus.Fatalf("Can not get kubernetes config: %v", err) 46 | } 47 | 48 | clientset, err := kubernetes.NewForConfig(config) 49 | if err != nil { 50 | logrus.Fatalf("Can not get kubernetes config: %v", err) 51 | } 52 | 53 | return clientset 54 | } 55 | 56 | // GetObjectMetaData returns metadata of a given k8s object 57 | func GetObjectMetaData(obj interface{}) (objectMeta meta_v1.ObjectMeta) { 58 | 59 | switch object := obj.(type) { 60 | case *apps_v1.Deployment: 61 | objectMeta = object.ObjectMeta 62 | case *api_v1.ReplicationController: 63 | objectMeta = object.ObjectMeta 64 | case *apps_v1.ReplicaSet: 65 | objectMeta = object.ObjectMeta 66 | case *apps_v1.DaemonSet: 67 | objectMeta = object.ObjectMeta 68 | case *api_v1.Service: 69 | objectMeta = object.ObjectMeta 70 | case *api_v1.Pod: 71 | objectMeta = object.ObjectMeta 72 | case *batch_v1.Job: 73 | objectMeta = object.ObjectMeta 74 | case *api_v1.PersistentVolume: 75 | objectMeta = object.ObjectMeta 76 | case *api_v1.Namespace: 77 | objectMeta = object.ObjectMeta 78 | case *api_v1.Secret: 79 | objectMeta = object.ObjectMeta 80 | case *ext_v1beta1.Ingress: 81 | objectMeta = object.ObjectMeta 82 | case *api_v1.Node: 83 | objectMeta = object.ObjectMeta 84 | case *rbac_v1beta1.ClusterRole: 85 | objectMeta = object.ObjectMeta 86 | case *api_v1.ServiceAccount: 87 | objectMeta = object.ObjectMeta 88 | case *api_v1.Event: 89 | objectMeta = object.ObjectMeta 90 | } 91 | return objectMeta 92 | } 93 | -------------------------------------------------------------------------------- /tools/yannotated/yannotated.go: -------------------------------------------------------------------------------- 1 | // Yannotated generates an annotated yaml config boilerplate from a Go structure tree. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "go/ast" 8 | "go/parser" 9 | "go/token" 10 | "io" 11 | "log" 12 | "os" 13 | "strings" 14 | 15 | "github.com/fatih/structtag" 16 | "github.com/segmentio/textio" 17 | ) 18 | 19 | const ( 20 | YAMLFormat = "yaml" 21 | GoFormat = "go" 22 | ) 23 | 24 | type Flags struct { 25 | // Dir is the directory containing the source code. 26 | Dir string 27 | // Package of the root type name. 28 | Package string 29 | // Type is the name of the root type of the config tree. 30 | Type string 31 | // Output is the file name of the generated output. 32 | Output string 33 | // Format is the format of the output. 34 | Format string 35 | } 36 | 37 | func (f *Flags) Bind(fs *flag.FlagSet) { 38 | if fs == nil { 39 | fs = flag.CommandLine 40 | } 41 | fs.StringVar(&f.Dir, "dir", ".", "Directory of the Go source code") 42 | fs.StringVar(&f.Package, "package", "", "Name of the root struct type") 43 | fs.StringVar(&f.Type, "type", "", "Name of the root struct type") 44 | fs.StringVar(&f.Output, "o", "", "Filename of the generated output") 45 | fs.StringVar(&f.Format, "format", "", "Output format: Yaml, Go") 46 | } 47 | 48 | func mainE(flags Flags) error { 49 | var fset token.FileSet 50 | pkgs, err := parser.ParseDir(&fset, flags.Dir, nil, parser.ParseComments) 51 | if err != nil { 52 | return err 53 | } 54 | pkg, found := pkgs[flags.Package] 55 | if !found { 56 | return fmt.Errorf("cannot find package %q in %v", flags.Package, pkgs) 57 | } 58 | 59 | // trim all unexported symbols 60 | for _, f := range pkg.Files { 61 | ast.FileExports(f) 62 | } 63 | 64 | types := collectTypes(pkg) 65 | 66 | root, found := types[flags.Type] 67 | if !found { 68 | return fmt.Errorf("cannot find root type %q in %v", flags.Type, types) 69 | } 70 | w, err := os.Create(flags.Output) 71 | if err != nil { 72 | return err 73 | } 74 | defer w.Close() 75 | 76 | if flags.Format == GoFormat { 77 | fmt.Fprintf(w, "package %s\n\n", flags.Package) 78 | fmt.Fprintf(w, "var yannotated = `") 79 | defer fmt.Fprintf(w, "`\n") 80 | } 81 | 82 | return emit(w, types, root) 83 | } 84 | 85 | func emit(w io.Writer, types map[string]*ast.StructType, node *ast.StructType) error { 86 | for _, field := range node.Fields.List { 87 | name, err := fieldName(field) 88 | if err != nil { 89 | return err 90 | } 91 | if field.Doc != nil { 92 | lines := strings.Split(strings.TrimSpace(field.Doc.Text()), "\n") 93 | for _, l := range lines { 94 | fmt.Fprintf(w, "# %s\n", l) 95 | } 96 | } 97 | 98 | fmt.Fprintf(w, "%s:", name) 99 | 100 | switch typ := field.Type.(type) { 101 | case *ast.Ident: 102 | 103 | switch name := typ.Name; name { 104 | case "string": 105 | fmt.Fprintln(w, ` ""`) 106 | case "int": 107 | fmt.Fprintln(w, " 0") 108 | case "bool": 109 | fmt.Fprintln(w, " false") 110 | default: 111 | t, found := types[name] 112 | if !found { 113 | return fmt.Errorf("cannot find type %q", name) 114 | } 115 | fmt.Fprintf(w, "\n") 116 | iw := textio.NewPrefixWriter(w, " ") 117 | if err := emit(iw, types, t); err != nil { 118 | return err 119 | } 120 | } 121 | case *ast.MapType: 122 | fmt.Fprintf(w, " {}\n") 123 | default: 124 | return fmt.Errorf("unsupported field type: %T (%s)", field.Type, field.Type) 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | func fieldName(field *ast.Field) (string, error) { 131 | if field.Tag != nil { 132 | // remove backticks 133 | clean := field.Tag.Value[1 : len(field.Tag.Value)-1] 134 | tags, err := structtag.Parse(clean) 135 | if err != nil { 136 | return "", fmt.Errorf("while parsing %q: %w", clean, err) 137 | } 138 | 139 | var yamlName, jsonName string 140 | for _, tag := range tags.Tags() { 141 | switch tag.Key { 142 | case "json": 143 | jsonName = tag.Name 144 | case "yaml": 145 | yamlName = tag.Name 146 | } 147 | } 148 | if yamlName != "" { 149 | return yamlName, nil 150 | } 151 | if jsonName != "" { 152 | return jsonName, nil 153 | } 154 | } 155 | if got, want := len(field.Names), 1; got != want { 156 | return "", fmt.Errorf("unsupported number of struct field names, got: %d, want: %d", got, want) 157 | } 158 | 159 | return strings.ToLower(field.Names[0].Name), nil 160 | } 161 | 162 | func collectTypes(n ast.Node) map[string]*ast.StructType { 163 | v := typeCollectingVisitor(map[string]*ast.StructType{}) 164 | ast.Walk(v, n) 165 | return v 166 | } 167 | 168 | type typeCollectingVisitor map[string]*ast.StructType 169 | 170 | func (v typeCollectingVisitor) Visit(node ast.Node) (w ast.Visitor) { 171 | switch n := node.(type) { 172 | case *ast.TypeSpec: 173 | if t, ok := n.Type.(*ast.StructType); ok { 174 | v[n.Name.Name] = t 175 | } 176 | case *ast.Package: 177 | return v 178 | case *ast.File: 179 | return v 180 | case *ast.GenDecl: 181 | if n.Tok == token.TYPE { 182 | return v 183 | } 184 | } 185 | return nil 186 | } 187 | 188 | func main() { 189 | var flags Flags 190 | flags.Bind(nil) 191 | flag.Parse() 192 | 193 | if err := mainE(flags); err != nil { 194 | log.Fatal(err) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /tools/yannotated/yannotated_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | // Config is a config. 10 | type Config struct { 11 | // Foo is foo. 12 | Foo string `yaml:"foo"` 13 | // Bar is bar. 14 | // So useful. 15 | Bar Bar `yaml:"bar"` 16 | // Rebar is another bar. 17 | Rebar Bar `yaml:"rebar"` 18 | Quz map[string]string 19 | } 20 | 21 | // Bar is a struct. 22 | type Bar struct { 23 | // Baz is baz. 24 | Baz int `yaml:"baz"` 25 | } 26 | 27 | func TestMain(t *testing.T) { 28 | tmp, err := ioutil.TempFile("", "") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | tmp.Close() 33 | defer os.RemoveAll(tmp.Name()) 34 | 35 | err = mainE(Flags{ 36 | Dir: ".", 37 | Package: "main", 38 | Type: "Config", 39 | Output: tmp.Name(), 40 | }) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | want := `# Foo is foo. 46 | foo: "" 47 | # Bar is bar. 48 | # So useful. 49 | bar: 50 | # Baz is baz. 51 | baz: 0 52 | # Rebar is another bar. 53 | rebar: 54 | # Baz is baz. 55 | baz: 0 56 | quz: {} 57 | ` 58 | b, err := ioutil.ReadFile(tmp.Name()) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | if got := string(b); got != want { 64 | t.Fatalf("got:\n%s\nwant:\n%s", got, want) 65 | } 66 | } 67 | 68 | func TestGo(t *testing.T) { 69 | tmp, err := ioutil.TempFile("", "") 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | tmp.Close() 74 | defer os.RemoveAll(tmp.Name()) 75 | 76 | err = mainE(Flags{ 77 | Dir: ".", 78 | Package: "main", 79 | Type: "Bar", 80 | Output: tmp.Name(), 81 | Format: GoFormat, 82 | }) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | want := `package main 88 | 89 | var yannotated = ` + "`" + `# Baz is baz. 90 | baz: 0 91 | ` + "`\n" 92 | 93 | b, err := ioutil.ReadFile(tmp.Name()) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | if got := string(b); got != want { 99 | t.Fatalf("got:\n%s\nwant:\n%s", got, want) 100 | } 101 | } 102 | --------------------------------------------------------------------------------