From bc43dacf4a31af39794e057e00500bf968b38ee5 Mon Sep 17 00:00:00 2001 From: Daniel Holbach Date: Wed, 17 Jun 2020 12:07:29 +0200 Subject: [PATCH] Create empty gh-pages branch We will use Github pages for publishing our helm chart via https://github.com/stefanprodan/helm-gh-pages --- .circleci/config.yml | 26 --- .github/workflows/check-links.yaml | 16 -- .gitignore | 4 - DEVELOPMENT.md | 106 --------- LICENSE | 191 ---------------- Makefile | 35 --- README.md | 307 ------------------------- cmd/kured/Dockerfile | 7 - cmd/kured/main.go | 356 ----------------------------- cmd/kured/pflags.go | 31 --- cmd/prom-active-alerts/main.go | 23 -- go.mod | 15 -- go.sum | 232 ------------------- img/logo.png | Bin 15937 -> 0 bytes img/slack-notification.png | Bin 7345 -> 0 bytes kured-ds.yaml | 63 ----- kured-rbac.yaml | 63 ----- pkg/alerts/prometheus.go | 52 ----- pkg/daemonsetlock/daemonsetlock.go | 123 ---------- pkg/delaytick/delaytick.go | 22 -- pkg/notifications/slack/slack.go | 52 ----- pkg/timewindow/days.go | 91 -------- pkg/timewindow/days_test.go | 46 ---- pkg/timewindow/timewindow.go | 68 ------ pkg/timewindow/timewindow_test.go | 60 ----- 25 files changed, 1989 deletions(-) delete mode 100644 .circleci/config.yml delete mode 100644 .github/workflows/check-links.yaml delete mode 100644 .gitignore delete mode 100644 DEVELOPMENT.md delete mode 100644 LICENSE delete mode 100644 Makefile delete mode 100644 README.md delete mode 100644 cmd/kured/Dockerfile delete mode 100644 cmd/kured/main.go delete mode 100644 cmd/kured/pflags.go delete mode 100644 cmd/prom-active-alerts/main.go delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 img/logo.png delete mode 100644 img/slack-notification.png delete mode 100644 kured-ds.yaml delete mode 100644 kured-rbac.yaml delete mode 100644 pkg/alerts/prometheus.go delete mode 100644 pkg/daemonsetlock/daemonsetlock.go delete mode 100644 pkg/delaytick/delaytick.go delete mode 100644 pkg/notifications/slack/slack.go delete mode 100644 pkg/timewindow/days.go delete mode 100644 pkg/timewindow/days_test.go delete mode 100644 pkg/timewindow/timewindow.go delete mode 100644 pkg/timewindow/timewindow_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 4167110..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: cimg/go:1.13 - steps: - - checkout - - setup_remote_docker - - deploy: - name: Build and push image - command: | - echo "$DOCKER_PASS" | docker login --username "$DOCKER_USER" --password-stdin - if [ -z "${CIRCLE_TAG}" ]; then - make publish-image - else - make VERSION="${CIRCLE_TAG}" publish-image - fi - -workflows: - version: 2 - build: - jobs: - - build: - filters: - tags: - only: /.*/ diff --git a/.github/workflows/check-links.yaml b/.github/workflows/check-links.yaml deleted file mode 100644 index fa50239..0000000 --- a/.github/workflows/check-links.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: "Check links" -on: [pull_request, push] - -jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Link Checker - id: lc - uses: peter-evans/link-checker@v1 - with: - args: -r *.md *.yaml */*/*.go -x .cluster.local - - name: Fail if there were link errors - run: exit ${{ steps.lc.outputs.exit_code }} - diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 400ca4d..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -cmd/kured/kured -cmd/prom-active-alerts/prom-active-alerts -vendor -build diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index 3dff98c..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,106 +0,0 @@ -# Developing `kured` - -We love contributions to `kured`, no matter if you are [helping out on -Slack][slack], reporting or triaging [issues][issues] or contributing code -to `kured`. - -In any case, it will make sense to familiarise yourself with the main -[README][readme] to understand the different features and options, which is -helpful for testing. The "building" section in particular makes sense if -you are planning to contribute code. - -[slack]: README.md#getting-help -[issues]: https://github.com/weaveworks/kured/issues -[readme]: README.md - -## Updating k8s support - -Whenever we want to update e.g. [`kubectl` in the -image](cmd/kured/Dockerfile), we need to consider if we update `client-go` -as well, some RBAC changes might be necessary too. - -This is what it took to support Kubernetes 1.14: - - -That the process can be more involved that that can be seen in - - -Once you updated everything, make sure you update the support matrix on -the main [README][readme] as well. - -## Release testing - -Before `kured` is released, we want to make sure it still works fine on the -previous, current and next minor version of Kubernetes (with respect to the -embedded `client-go` & `kubectl`). For local testing e.g. `minikube` can be -sufficient. - -Deploy kured in your test scenario, make sure you pass the right `image`, -update the e.g. `period` and `reboot-days` options, so you get immediate -results, if you login to a node and run: - -```console -sudo touch /var/run/reboot-required -``` - -### Testing with `minikube` - -A test-run with `minikube` could look like this: - -```console -minikube start --vm-driver kvm2 --kubernetes-version - -# edit kured-ds.yaml to -# - point to new image -# - change e.g. period and reboot-days option for immediate results - -minikube kubectl -- apply -f kured-rbac.yaml -minikube kubectl -- apply -f kured-ds.yaml -minikube kubectl -- logs daemonset.apps/kured -n kube-system -f - -# In separate terminal -minikube ssh - sudo touch /var/run/reboot-required -minikube logs -f -``` - -Now check for the 'Commanding reboot' message and minikube going down. - -Unfortunately as of today, you are going to run into -. This means that -minikube won't come back easily. You will need to start minikube again. -Then you can check for the lock release. - -If all the tests ran well, kured maintainers can reach out to the Weaveworks -team to get an upcoming `kured` release tested in the Dev environment for -real life testing. - -## Publishing a new kured release - -Check that `README.md` has an updated compatibility matrix and that the -url in the `kubectl` incantation (under "Installation") is updated to the -new version you want to release. - -Now create the `kured--dockerhub.yaml` for e.g. `1.3.0`: - -```sh -VERSION=1.3.0 -MANIFEST="kured-$VERSION-dockerhub.yaml" -cat kured-rbac.yaml > "$MANIFEST" -cat kured-ds.yaml >> "$MANIFEST" -sed -i "s#docker.io/weaveworks/kured#docker.io/weaveworks/kured:$VERSION#g" "$MANIFEST" -``` - -The last thing you need to do is update the `image:` to point to the release -tag, e.g. `docker.io/weaveworks/kured:1.3.0`. - -Now you can head to the Github UI, use the version number as tag and upload the -`kured--dockerhub.yaml` file. - -### Release notes - -Please describe what's new and noteworthy in the release notes, list the PRs -that landed and give a shout-out to everyone who contributed. - -Please also note down on which releases the upcoming `kured` release was -tested on. (Check old release notes if you're unsure.) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 64a35d0..0000000 --- a/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2017 Weaveworks Ltd. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/Makefile b/Makefile deleted file mode 100644 index e977440..0000000 --- a/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -.DEFAULT: all -.PHONY: all clean image publish-image minikube-publish - -DH_ORG=weaveworks -VERSION=$(shell git symbolic-ref --short HEAD)-$(shell git rev-parse --short HEAD) -SUDO=$(shell docker info >/dev/null 2>&1 || echo "sudo -E") - -all: image - -clean: - rm -f cmd/kured/kured - rm -rf ./build - -godeps=$(shell go list -f '{{join .Deps "\n"}}' $1 | grep -v /vendor/ | xargs go list -f '{{if not .Standard}}{{ $$dep := . }}{{range .GoFiles}}{{$$dep.Dir}}/{{.}} {{end}}{{end}}') - -DEPS=$(call godeps,./cmd/kured) - -cmd/kured/kured: $(DEPS) -cmd/kured/kured: cmd/kured/*.go - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o $@ cmd/kured/*.go - -build/.image.done: cmd/kured/Dockerfile cmd/kured/kured - mkdir -p build - cp $^ build - $(SUDO) docker build -t docker.io/$(DH_ORG)/kured -f build/Dockerfile ./build - $(SUDO) docker tag docker.io/$(DH_ORG)/kured docker.io/$(DH_ORG)/kured:$(VERSION) - touch $@ - -image: build/.image.done - -publish-image: image - $(SUDO) docker push docker.io/$(DH_ORG)/kured:$(VERSION) - -minikube-publish: image - $(SUDO) docker save docker.io/$(DH_ORG)/kured | (eval $$(minikube docker-env) && docker load) diff --git a/README.md b/README.md deleted file mode 100644 index 346a114..0000000 --- a/README.md +++ /dev/null @@ -1,307 +0,0 @@ - - - -* [Introduction](#introduction) -* [Kubernetes & OS Compatibility](#kubernetes-&-os-compatibility) -* [Installation](#installation) -* [Configuration](#configuration) - * [Reboot Sentinel File & Period](#reboot-sentinel-file-&-period) - * [Setting a schedule](#setting-a-schedule) - * [Blocking Reboots via Alerts](#blocking-reboots-via-alerts) - * [Blocking Reboots via Pods](#blocking-reboots-via-pods) - * [Prometheus Metrics](#prometheus-metrics) - * [Slack Notifications](#slack-notifications) - * [Overriding Lock Configuration](#overriding-lock-configuration) -* [Operation](#operation) - * [Testing](#testing) - * [Disabling Reboots](#disabling-reboots) - * [Manual Unlock](#manual-unlock) -* [Building](#building) -* [Frequently Asked/Anticipated Questions](#frequently-askedanticipated-questions) -* [Getting Help](#getting-help) - -## Introduction - -Kured (KUbernetes REboot Daemon) is a Kubernetes daemonset that -performs safe automatic node reboots when the need to do so is -indicated by the package management system of the underlying OS. - -* Watches for the presence of a reboot sentinel e.g. `/var/run/reboot-required` -* Utilises a lock in the API server to ensure only one node reboots at - a time -* Optionally defers reboots in the presence of active Prometheus alerts or selected pods -* Cordons & drains worker nodes before reboot, uncordoning them after - -## Kubernetes & OS Compatibility - -The daemon image contains versions of `k8s.io/client-go` and the -`kubectl` binary for the purposes of maintaining the lock and draining -worker nodes. Kubernetes aims to provide forwards & backwards -compatibility of one minor version between client and server: - -| kured | kubectl | k8s.io/client-go | k8s.io/apimachinery | expected kubernetes compatibility | -|--------|---------|------------------|---------------------|-----------------------------------| -| master | 1.18.2 | v0.18.2 | v0.18.2 | 1.17.x, 1.18.x, 1.19.x | -| 1.4.0 | 1.17.5 | v0.17.0 | v0.17.0 | 1.16.x, 1.17.x, 1.18.x | -| 1.3.0 | 1.15.10 | v12.0.0 | release-1.15 | 1.15.x, 1.16.x, 1.17.x | -| 1.2.0 | 1.13.6 | v10.0.0 | release-1.13 | 1.12.x, 1.13.x, 1.14.x | -| 1.1.0 | 1.12.1 | v9.0.0 | release-1.12 | 1.11.x, 1.12.x, 1.13.x | -| 1.0.0 | 1.7.6 | v4.0.0 | release-1.7 | 1.6.x, 1.7.x, 1.8.x | - -See the [release notes](https://github.com/weaveworks/kured/releases) -for specific version compatibility information, including which -combination have been formally tested. - -Versions >=1.1.0 enter the host mount namespace to invoke -`systemctl reboot`, so should work on any systemd distribution. - -## Installation - -To obtain a default installation without Prometheus alerting interlock -or Slack notifications: - -``` -kubectl apply -f https://github.com/weaveworks/kured/releases/download/1.3.0/kured-1.3.0-dockerhub.yaml -``` - -If you want to customise the installation, download the manifest and -edit it in accordance with the following section before application. - -## Configuration - -The following arguments can be passed to kured via the daemonset pod template: - -``` -Flags: - --alert-filter-regexp regexp.Regexp alert names to ignore when checking for active alerts - --blocking-pod-selector stringArray label selector identifying pods whose presence should prevent reboots - --ds-name string name of daemonset on which to place lock (default "kured") - --ds-namespace string namespace containing daemonset on which to place lock (default "kube-system") - --end-time string only reboot before this time of day (default "23:59") - -h, --help help for kured - --lock-annotation string annotation in which to record locking node (default "weave.works/kured-node-lock") - --period duration reboot check period (default 1h0m0s) - --prometheus-url string Prometheus instance to probe for active alerts - --reboot-days strings only reboot on these days (default [su,mo,tu,we,th,fr,sa]) - --reboot-sentinel string path to file whose existence signals need to reboot (default "/var/run/reboot-required") - --slack-channel string slack channel for reboot notfications - --slack-hook-url string slack hook URL for reboot notfications - --slack-username string slack username for reboot notfications (default "kured") - --start-time string only reboot after this time of day (default "0:00") - --time-zone string use this timezone to calculate allowed reboot time (default "UTC") -``` - -### Reboot Sentinel File & Period - -By default kured checks for the existence of -`/var/run/reboot-required` every sixty minutes; you can override these -values with `--reboot-sentinel` and `--period`. Each replica of the -daemon uses a random offset derived from the period on startup so that -nodes don't all contend for the lock simultaneously. - -### Setting a schedule - -By default, kured will reboot any time it detects the sentinel, but this -may cause reboots during odd hours. While service disruption does not -normally occur, anything is possible and operators may want to restrict -reboots to predictable schedules. Use `--reboot-days`, `--start-time`, -`--end-time`, and `--time-zone` to set a schedule. For example, business -hours on the west coast USA can be specified with: - -``` - --reboot-days mon,tue,wed,thu,fri - --start-time 9am - --end-time 5pm - --time-zone America/Los_Angeles -``` - -Times can be formatted in numerous ways, including `5pm`, `5:00pm` `17:00`, -and `17`. `--time-zone` represents a Go `time.Location`, and can be `UTC`, -`Local`, or any entry in the standard Linux tz database. - -Note that when using smaller time windows, you should consider shortening -the sentinel check period (`--period`). - -### Blocking Reboots via Alerts - -You may find it desirable to block automatic node reboots when there -are active alerts - you can do so by providing the URL of your -Prometheus server: - -``` ---prometheus-url=http://prometheus.monitoring.svc.cluster.local -``` - -By default the presence of *any* active (pending or firing) alerts -will block reboots, however you can ignore specific alerts: - -``` ---alert-filter-regexp=^(RebootRequired|AnotherBenignAlert|...$ -``` - -See the section on Prometheus metrics for an important application of this -filter. - -### Blocking Reboots via Pods - -You can also block reboots of an _individual node_ when specific pods -are scheduled on it: - -``` ---blocking-pod-selector=runtime=long,cost=expensive -``` - -Since label selector strings use commas to express logical 'and', you can -specify this parameter multiple times for 'or': - -``` ---blocking-pod-selector=runtime=long,cost=expensive ---blocking-pod-selector=name=temperamental -``` - -In this case, the presence of either an (appropriately labelled) expensive long -running job or a known temperamental pod on a node will stop it rebooting. - -> Try not to abuse this mechanism - it's better to strive for -> restartability where possible. If you do use it, make sure you set -> up a RebootRequired alert as described in the next section so that -> you can intervene manually if reboots are blocked for too long. - -### Prometheus Metrics - -Each kured pod exposes a single gauge metric (`:8080/metrics`) that -indicates the presence of the sentinel file: - -``` -# HELP kured_reboot_required OS requires reboot due to software updates. -# TYPE kured_reboot_required gauge -kured_reboot_required{node="ip-xxx-xxx-xxx-xxx.ec2.internal"} 0 -``` - -The purpose of this metric is to power an alert which will summon an -operator if the cluster cannot reboot itself automatically for a -prolonged period: - -``` -# Alert if a reboot is required for any machines. Acts as a failsafe for the -# reboot daemon, which will not reboot nodes if there are pending alerts save -# this one. -ALERT RebootRequired - IF max(kured_reboot_required) != 0 - FOR 24h - LABELS { severity="warning" } - ANNOTATIONS { - summary = "Machine(s) require being rebooted, and the reboot daemon has failed to do so for 24 hours", - impact = "Cluster nodes more vulnerable to security exploits. Eventually, no disk space left.", - description = "Machine(s) require being rebooted, probably due to kernel update.", - } -``` - -If you choose to employ such an alert and have configured kured to -probe for active alerts before rebooting, be sure to specify -`--alert-filter-regexp=^RebootRequired$` to avoid deadlock! - -### Slack Notifications - -If you specify a Slack hook via `--slack-hook-url`, kured will notify -you immediately prior to rebooting a node: - - - -We recommend setting `--slack-username` to be the name of the -environment, e.g. `dev` or `prod`. - -### Overriding Lock Configuration - -The `--ds-name` and `--ds-namespace` arguments should match the name and -namespace of the daemonset used to deploy the reboot daemon - the locking is -implemented by means of an annotation on this resource. The defaults match -the daemonset YAML provided in the repository. - -Similarly `--lock-annotation` can be used to change the name of the -annotation kured will use to store the lock, but the default is almost -certainly safe. - -## Operation - -The example commands in this section assume that you have not -overriden the default lock annotation, daemonset name or namespace; -if you have, you will have to adjust the commands accordingly. - -### Testing - -You can test your configuration by provoking a reboot on a node: - -``` -sudo touch /var/run/reboot-required -``` - -### Disabling Reboots - -If you need to temporarily stop kured from rebooting any nodes, you -can take the lock manually: - -``` -kubectl -n kube-system annotate ds kured weave.works/kured-node-lock='{"nodeID":"manual"}' -``` - -Don't forget to release it afterwards! - -### Manual Unlock - -In exceptional circumstances, such as a node experiencing a permanent -failure whilst rebooting, manual intervention may be required to -remove the cluster lock: - -``` -kubectl -n kube-system annotate ds kured weave.works/kured-node-lock- -``` -> NB the `-` at the end of the command is important - it instructs -> `kubectl` to remove that annotation entirely. - -## Building - -See the [CircleCI config](.circleci/config.yml) for the preferred -version of Golang. Kured now uses [Go -Modules](https://github.com/golang/go/wiki/Modules), so build -instructions vary depending on where you have checked out the -repository: - -**Building outside $GOPATH:** - -``` -make -``` - -**Building inside $GOPATH:** - -``` -GO111MODULE=on make -``` - -If you are interested in contributing code to kured, please take a look at -our [development][development] docs. - -[development]: DEVELOPMENT.md - -## Frequently Asked/Anticipated Questions - -### Why is there no `latest` tag on Docker Hub? - -Use of `latest` for production deployments is bad practice - see -[here](https://kubernetes.io/docs/concepts/configuration/overview) for -details. The manifest on `master` refers to `latest` for local -development testing with minikube only; for production use choose a -versioned manifest from the [release page](https://github.com/weaveworks/kured/releases/). - -## Getting Help - -If you have any questions about, feedback for or problems with `kured`: - -- Invite yourself to the Weave Users Slack. -- Ask a question on the [#kured](https://weave-community.slack.com/messages/kured/) slack channel. -- [File an issue](https://github.com/weaveworks/kured/issues/new). -- Join us in [our monthly meeting](https://docs.google.com/document/d/1bsHTjHhqaaZ7yJnXF6W8c89UB_yn-OoSZEmDnIP34n8/edit#), - every fourth Wednesday of the month at 16:00 UTC. - -Your feedback is always welcome! diff --git a/cmd/kured/Dockerfile b/cmd/kured/Dockerfile deleted file mode 100644 index e835e8c..0000000 --- a/cmd/kured/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM alpine:3.11 -RUN apk update && apk add ca-certificates tzdata && rm -rf /var/cache/apk/* -# NB: you may need to update RBAC permissions when upgrading kubectl - see kured-rbac.yaml for details -ADD https://storage.googleapis.com/kubernetes-release/release/v1.18.2/bin/linux/amd64/kubectl /usr/bin/kubectl -RUN chmod 0755 /usr/bin/kubectl -COPY ./kured /usr/bin/kured -ENTRYPOINT ["/usr/bin/kured"] diff --git a/cmd/kured/main.go b/cmd/kured/main.go deleted file mode 100644 index 482525c..0000000 --- a/cmd/kured/main.go +++ /dev/null @@ -1,356 +0,0 @@ -package main - -import ( - "context" - "fmt" - "math/rand" - "net/http" - "os" - "os/exec" - "regexp" - "time" - - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/weaveworks/kured/pkg/alerts" - "github.com/weaveworks/kured/pkg/daemonsetlock" - "github.com/weaveworks/kured/pkg/delaytick" - "github.com/weaveworks/kured/pkg/notifications/slack" - "github.com/weaveworks/kured/pkg/timewindow" -) - -var ( - version = "unreleased" - - // Command line flags - period time.Duration - dsNamespace string - dsName string - lockAnnotation string - prometheusURL string - alertFilter *regexp.Regexp - rebootSentinel string - slackHookURL string - slackUsername string - slackChannel string - podSelectors []string - - rebootDays []string - rebootStart string - rebootEnd string - timezone string - - // Metrics - rebootRequiredGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Subsystem: "kured", - Name: "reboot_required", - Help: "OS requires reboot due to software updates.", - }, []string{"node"}) -) - -func init() { - prometheus.MustRegister(rebootRequiredGauge) -} - -func main() { - rootCmd := &cobra.Command{ - Use: "kured", - Short: "Kubernetes Reboot Daemon", - Run: root} - - rootCmd.PersistentFlags().DurationVar(&period, "period", time.Minute*60, - "reboot check period") - rootCmd.PersistentFlags().StringVar(&dsNamespace, "ds-namespace", "kube-system", - "namespace containing daemonset on which to place lock") - rootCmd.PersistentFlags().StringVar(&dsName, "ds-name", "kured", - "name of daemonset on which to place lock") - rootCmd.PersistentFlags().StringVar(&lockAnnotation, "lock-annotation", "weave.works/kured-node-lock", - "annotation in which to record locking node") - rootCmd.PersistentFlags().StringVar(&prometheusURL, "prometheus-url", "", - "Prometheus instance to probe for active alerts") - rootCmd.PersistentFlags().Var(®expValue{&alertFilter}, "alert-filter-regexp", - "alert names to ignore when checking for active alerts") - rootCmd.PersistentFlags().StringVar(&rebootSentinel, "reboot-sentinel", "/var/run/reboot-required", - "path to file whose existence signals need to reboot") - - rootCmd.PersistentFlags().StringVar(&slackHookURL, "slack-hook-url", "", - "slack hook URL for reboot notfications") - rootCmd.PersistentFlags().StringVar(&slackUsername, "slack-username", "kured", - "slack username for reboot notfications") - rootCmd.PersistentFlags().StringVar(&slackChannel, "slack-channel", "", - "slack channel for reboot notfications") - - rootCmd.PersistentFlags().StringArrayVar(&podSelectors, "blocking-pod-selector", nil, - "label selector identifying pods whose presence should prevent reboots") - - rootCmd.PersistentFlags().StringSliceVar(&rebootDays, "reboot-days", timewindow.EveryDay, - "schedule reboot on these days") - rootCmd.PersistentFlags().StringVar(&rebootStart, "start-time", "0:00", - "schedule reboot only after this time of day") - rootCmd.PersistentFlags().StringVar(&rebootEnd, "end-time", "23:59:59", - "schedule reboot only before this time of day") - rootCmd.PersistentFlags().StringVar(&timezone, "time-zone", "UTC", - "use this timezone for schedule inputs") - - if err := rootCmd.Execute(); err != nil { - log.Fatal(err) - } -} - -// newCommand creates a new Command with stdout/stderr wired to our standard logger -func newCommand(name string, arg ...string) *exec.Cmd { - cmd := exec.Command(name, arg...) - - cmd.Stdout = log.NewEntry(log.StandardLogger()). - WithField("cmd", cmd.Args[0]). - WithField("std", "out"). - WriterLevel(log.InfoLevel) - - cmd.Stderr = log.NewEntry(log.StandardLogger()). - WithField("cmd", cmd.Args[0]). - WithField("std", "err"). - WriterLevel(log.WarnLevel) - - return cmd -} - -func sentinelExists() bool { - // Relies on hostPID:true and privileged:true to enter host mount space - sentinelCmd := newCommand("/usr/bin/nsenter", "-m/proc/1/ns/mnt", "--", "/usr/bin/test", "-f", rebootSentinel) - if err := sentinelCmd.Run(); err != nil { - switch err := err.(type) { - case *exec.ExitError: - // We assume a non-zero exit code means 'reboot not required', but of course - // the user could have misconfigured the sentinel command or something else - // went wrong during its execution. In that case, not entering a reboot loop - // is the right thing to do, and we are logging stdout/stderr of the command - // so it should be obvious what is wrong. - return false - default: - // Something was grossly misconfigured, such as the command path being wrong. - log.Fatalf("Error invoking sentinel command: %v", err) - } - } - return true -} - -func rebootRequired() bool { - if sentinelExists() { - log.Infof("Reboot required") - return true - } else { - log.Infof("Reboot not required") - return false - } -} - -func rebootBlocked(client *kubernetes.Clientset, nodeID string) bool { - if prometheusURL != "" { - alertNames, err := alerts.PrometheusActiveAlerts(prometheusURL, alertFilter) - if err != nil { - log.Warnf("Reboot blocked: prometheus query error: %v", err) - return true - } - count := len(alertNames) - if count > 10 { - alertNames = append(alertNames[:10], "...") - } - if count > 0 { - log.Warnf("Reboot blocked: %d active alerts: %v", count, alertNames) - return true - } - } - - fieldSelector := fmt.Sprintf("spec.nodeName=%s", nodeID) - for _, labelSelector := range podSelectors { - podList, err := client.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{ - LabelSelector: labelSelector, - FieldSelector: fieldSelector, - Limit: 10}) - if err != nil { - log.Warnf("Reboot blocked: pod query error: %v", err) - return true - } - - if len(podList.Items) > 0 { - podNames := make([]string, 0, len(podList.Items)) - for _, pod := range podList.Items { - podNames = append(podNames, pod.Name) - } - if len(podList.Continue) > 0 { - podNames = append(podNames, "...") - } - log.Warnf("Reboot blocked: matching pods: %v", podNames) - return true - } - } - - return false -} - -func holding(lock *daemonsetlock.DaemonSetLock, metadata interface{}) bool { - holding, err := lock.Test(metadata) - if err != nil { - log.Fatalf("Error testing lock: %v", err) - } - if holding { - log.Infof("Holding lock") - } - return holding -} - -func acquire(lock *daemonsetlock.DaemonSetLock, metadata interface{}) bool { - holding, holder, err := lock.Acquire(metadata) - switch { - case err != nil: - log.Fatalf("Error acquiring lock: %v", err) - return false - case !holding: - log.Warnf("Lock already held: %v", holder) - return false - default: - log.Infof("Acquired reboot lock") - return true - } -} - -func release(lock *daemonsetlock.DaemonSetLock) { - log.Infof("Releasing lock") - if err := lock.Release(); err != nil { - log.Fatalf("Error releasing lock: %v", err) - } -} - -func drain(nodeID string) { - log.Infof("Draining node %s", nodeID) - - if slackHookURL != "" { - if err := slack.NotifyDrain(slackHookURL, slackUsername, slackChannel, nodeID); err != nil { - log.Warnf("Error notifying slack: %v", err) - } - } - - drainCmd := newCommand("/usr/bin/kubectl", "drain", - "--ignore-daemonsets", "--delete-local-data", "--force", nodeID) - - if err := drainCmd.Run(); err != nil { - log.Fatalf("Error invoking drain command: %v", err) - } -} - -func uncordon(nodeID string) { - log.Infof("Uncordoning node %s", nodeID) - uncordonCmd := newCommand("/usr/bin/kubectl", "uncordon", nodeID) - if err := uncordonCmd.Run(); err != nil { - log.Fatalf("Error invoking uncordon command: %v", err) - } -} - -func commandReboot(nodeID string) { - log.Infof("Commanding reboot for node: %s", nodeID) - - if slackHookURL != "" { - if err := slack.NotifyReboot(slackHookURL, slackUsername, slackChannel, nodeID); err != nil { - log.Warnf("Error notifying slack: %v", err) - } - } - - // Relies on hostPID:true and privileged:true to enter host mount space - rebootCmd := newCommand("/usr/bin/nsenter", "-m/proc/1/ns/mnt", "/bin/systemctl", "reboot") - if err := rebootCmd.Run(); err != nil { - log.Fatalf("Error invoking reboot command: %v", err) - } -} - -func maintainRebootRequiredMetric(nodeID string) { - for { - if sentinelExists() { - rebootRequiredGauge.WithLabelValues(nodeID).Set(1) - } else { - rebootRequiredGauge.WithLabelValues(nodeID).Set(0) - } - time.Sleep(time.Minute) - } -} - -// nodeMeta is used to remember information across reboots -type nodeMeta struct { - Unschedulable bool `json:"unschedulable"` -} - -func rebootAsRequired(nodeID string, window *timewindow.TimeWindow) { - config, err := rest.InClusterConfig() - if err != nil { - log.Fatal(err) - } - - client, err := kubernetes.NewForConfig(config) - if err != nil { - log.Fatal(err) - } - - lock := daemonsetlock.New(client, nodeID, dsNamespace, dsName, lockAnnotation) - - nodeMeta := nodeMeta{} - if holding(lock, &nodeMeta) { - if !nodeMeta.Unschedulable { - uncordon(nodeID) - } - release(lock) - } - - source := rand.NewSource(time.Now().UnixNano()) - tick := delaytick.New(source, period) - for _ = range tick { - if window.Contains(time.Now()) && rebootRequired() && !rebootBlocked(client, nodeID) { - node, err := client.CoreV1().Nodes().Get(context.TODO(), nodeID, metav1.GetOptions{}) - if err != nil { - log.Fatal(err) - } - nodeMeta.Unschedulable = node.Spec.Unschedulable - - if acquire(lock, &nodeMeta) { - if !nodeMeta.Unschedulable { - drain(nodeID) - } - commandReboot(nodeID) - for { - log.Infof("Waiting for reboot") - time.Sleep(time.Minute) - } - } - } - } -} - -func root(cmd *cobra.Command, args []string) { - log.Infof("Kubernetes Reboot Daemon: %s", version) - - nodeID := os.Getenv("KURED_NODE_ID") - if nodeID == "" { - log.Fatal("KURED_NODE_ID environment variable required") - } - - window, err := timewindow.New(rebootDays, rebootStart, rebootEnd, timezone) - if err != nil { - log.Fatalf("Failed to build time window: %v", err) - } - - log.Infof("Node ID: %s", nodeID) - log.Infof("Lock Annotation: %s/%s:%s", dsNamespace, dsName, lockAnnotation) - log.Infof("Reboot Sentinel: %s every %v", rebootSentinel, period) - log.Infof("Blocking Pod Selectors: %v", podSelectors) - log.Infof("Reboot on: %v", window) - - go rebootAsRequired(nodeID, window) - go maintainRebootRequiredMetric(nodeID) - - http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(":8080", nil)) -} diff --git a/cmd/kured/pflags.go b/cmd/kured/pflags.go deleted file mode 100644 index b6eb5ab..0000000 --- a/cmd/kured/pflags.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "regexp" -) - -type regexpValue struct { - value **regexp.Regexp -} - -func (rev *regexpValue) String() string { - if *rev.value == nil { - return "" - } - return (*rev.value).String() -} - -func (rev *regexpValue) Set(s string) error { - value, err := regexp.Compile(s) - if err != nil { - return err - } - - *rev.value = value - - return nil -} - -func (rev *regexpValue) Type() string { - return "regexp.Regexp" -} diff --git a/cmd/prom-active-alerts/main.go b/cmd/prom-active-alerts/main.go deleted file mode 100644 index a2dec5c..0000000 --- a/cmd/prom-active-alerts/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "regexp" - - "github.com/weaveworks/kured/pkg/alerts" -) - -func main() { - if len(os.Args) != 3 { - log.Fatalf("USAGE: %s ", os.Args[0]) - } - - count, err := alerts.PrometheusCountActive(os.Args[1], regexp.MustCompile(os.Args[2])) - if err != nil { - log.Fatal(err) - } - - fmt.Println(count) -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 62b3b05..0000000 --- a/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module github.com/weaveworks/kured - -go 1.13 - -require ( - github.com/googleapis/gnostic v0.2.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/prometheus/client_golang v0.0.0-20181230203121-fb3d5cb2ad57 - github.com/prometheus/common v0.0.0-20181218105931-67670fe90761 - github.com/prometheus/procfs v0.0.0-20190102135031-14fa7590c24d // indirect - github.com/sirupsen/logrus v1.2.0 - github.com/spf13/cobra v0.0.0-20181127133106-d2d81d9a96e2 - k8s.io/apimachinery v0.18.2 - k8s.io/client-go v0.18.2 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index fb472de..0000000 --- a/go.sum +++ /dev/null @@ -1,232 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= -github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.0.0-20181230203121-fb3d5cb2ad57 h1:+cstagqzfjiYrs1QrmypZRndHJafhB9W3ab/zPiwl1Q= -github.com/prometheus/client_golang v0.0.0-20181230203121-fb3d5cb2ad57/go.mod h1:PS8H/EtMiYNSR3o4jWYMuyp9FkDf2ptwxn1kTerl5EM= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/common v0.0.0-20181218105931-67670fe90761 h1:z6tvbDJ5OLJ48FFmnksv04a78maSTRBUIhkdHYV5Y98= -github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190102135031-14fa7590c24d h1:iSxvUGRUGQTUgEo5+8TqMVQoH5AdaphEIjh8AnM7TPo= -github.com/prometheus/procfs v0.0.0-20190102135031-14fa7590c24d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cobra v0.0.0-20181127133106-d2d81d9a96e2 h1:gPjqutnNhLnq5t4hT2UfL06h/mvp4je4OY7Dk71vhuk= -github.com/spf13/cobra v0.0.0-20181127133106-d2d81d9a96e2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= -golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.18.0 h1:lwYk8Vt7rsVTwjRU6pzEsa9YNhThbmbocQlKvNBB4EQ= -k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= -k8s.io/api v0.18.2 h1:wG5g5ZmSVgm5B+eHMIbI9EGATS2L8Z72rda19RIEgY8= -k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= -k8s.io/apimachinery v0.18.0 h1:fuPfYpk3cs1Okp/515pAf0dNhL66+8zk8RLbSX+EgAE= -k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= -k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA= -k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= -k8s.io/client-go v0.18.0 h1:yqKw4cTUQraZK3fcVCMeSa+lqKwcjZ5wtcOIPnxQno4= -k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= -k8s.io/client-go v0.18.2 h1:aLB0iaD4nmwh7arT2wIn+lMnAq7OswjaejkQ8p9bBYE= -k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.3.0 h1:0VPpR+sizsiivjIfIAQH/rl8tan6jvWkS7lU+0di3lE= -k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= -k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU= -k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/img/logo.png b/img/logo.png deleted file mode 100644 index 50bd41c2e386609b0d7c29f49b20367c8c66bd75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15937 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 JQ7vW=_&-FpS33Xz diff --git a/img/slack-notification.png b/img/slack-notification.png deleted file mode 100644 index b8797c8b2db927f1b4ebe83488fb37c6cb78b2c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7345 zcmXYW1yohd_x3@Ml9KLj;gU*&ba&^a`x4TPAo2nNN{4hy*QHxpy4y=jcZ&Sa?_1wq zXPua{X4cH!d-i_zGf^69^4OTb4@RQ6_*1%KS)z;I;;)4xf=i&;p;qm75mW|JulNmzlw)EaV+kR#sMFw2@2=4GmA6B!Rr#+$ZqEhcO#D}OwOcGID*l55GC?lJ;uI*7-$h95%*}sqZ{M}R^6oJIykGeQpo`Ke z1qGgspJOAb13O#mVEG4Tr*PlOJPL58O7nhtI91V?);!Q$?Pzoi# z#P8Dsx#;c|FXIS88;;rWP_*hPvgYSW52u4OikCO;2--9WpYbQMC478*0*tS2WQ{UL z(f1}`IQ9~Wp!NiG*<@) z1dpQ*3RhigtN#$*@@M zZv6BN)YjUXoSfW})cZMjy7Q?EhOsR3uVHJ?G&VLC=(erJp%f(Ti7A}>`s>_$5e(qwtBMXp-(+DSC<&i?t|M#I(@R@NkWNg@u` z!QyB7RWjoo-LS?jCBVEh^`Vw%xuXp(bP-LfphgB4Q4_tlv!JFqEOn2>p>B5jx0(Q2 z>-Taa%6%wLtlUbwgVp|2Uu-NX7cV9d7WOU+L&S^u2p2_CD@VM;<95ArB??p$a5gx+ zg+P>aWeIH+^hfn?Mn_`?1O$}n7=gAm>hA6i?L(kP^zKGEIyyc)Je-0~VgIk6iOI{0 zUVab(bnkqiqWmlxoS&`q9W`R^|&aU6#@h0`WC$Qc2&*Fh|__rkroyy2?tFLNq`QIF10O1vtNI+jr zT^%oP&@8JD6iQ=koT*<8O|-?LH~@eS|7%`;ew0uWE;DNz8>IKQCu^!I{>Y&}ejLch zj1CX~nJPew>F4F)86F>Z3F?f9h+vI&baD!Db39#7PEP)Fco@R}{9}YYC`inYA>X*k zg@`EGwC&*XK5*LI+Qx2SZqC8c(cZ!9{EW1ysOTk7Zx__&xN||H7vk!?%J7#17g%(L zE1s@fGplQ9Jzn$exe3CEMl5TIf8`9CbK_76bEsETR*L!V*Iajh?Ccb$d7Bv~rAT%m1$PS*oOo zzs)d-_yu5(9(#nzLx#1ZJUlT$!()j8q@|^qboe)Rc4l8m>gwvILQM0g>@zYl#>dB< z-?ioCApsE6RvDaP14Bcv!};TxaW!@Ii3J}M9Z#&p9&mb8@0?jP$4~*ZNV>w%jO74{ z{q1ev%ZaXogM)3=FALQhg~f%aD0fXzlvugLwQ_w;&2OTzrlwSV+yXStUU)n(&ydX$w+xR2yNP{#JB7rs_WIV~ac&K9Y=Z&J1xZRl;3%6*B^l!~4E z_{XYD2NS@h7DWM6Rb$OM{NZ@}&tn4o0&Bn_Hvb=5>!Ss9p|n`0VOHTu0ZQ!C+ZYMK=ttwcFE zysJo-Q(awx1|U~fpc)x=1V8Q0AIrpRp1{hiNagTQV*2TEb0k8}&!!4mJf4yryopke(p8Abh}LwW8#6NnuF#n;Uw+X^ABlKkf>Cg?u7toteLnDEHyG7%Df5116QUyg z%YE2OkzA=7-n}!C!;qS)B)u;PG zqa(w^wY9b9cQk#GxD%tJedqDFcS(aoLkgy*Vr*<=0LqJ=zR38aBj=%EcWsr^b7^DU%rftz7ak@Vr4ZF&sort@cmsaCaGOl9&YQ%{PY7CAjQdB zI8ZaGW}#AJKowM54P?hgU`!3flX#yawRVTEophg|EiNyM`X%Ip`@&=Q%CNpum$m*w zKQBYy$?Nu^@83;0IXNY|qGE_40O%EC$d9+Ek8IcH{OUagZ8Qr2};hOx7=Tk{%T zDv|{S1%ZLBw^zyrruza#^6!%!R4|0+z6V)2>tyGTg`*=T&?sebXP6y7AqYuTR+?`Q z@Zm(XMnz#n_W3>D9B+hdSaR<(GcyM#GQa2^O(jdMHnFK+G7fKgjq;IpsJ#U#=pn5^ zn_oXN;50a)Ewj9(1xzKw!=TRwGM=hvYItJOR8*u6p`UU#;v0@4;RNsvAZ$#4p)G+5 zF~iJ>L4|m{mbic$_}#%)F(vv7tOkdbJx`$zx=9%`ldbKCJ*ND+61=)9RRTz%<@#J? zp2rVSc5;%E=g*^PxNrrjTx`q%CA*fg*C?9tn;mV_Rw1+L+e4CgwCHK|`fk@e%k6fb zXw7Y9XzY6pb_?>{yjzO<2t@ve@=$56JTcj8<~x7pv#jOOXd9@1Su<&IZ{4*w9)DmX z--y+1yVKyEk1NQ3fe2ZMd^Y9MZ_k9hDI4JMpFSQVjbCrT63&FiksSsV!YpkZ(lajh zXC|V;!Ztn5dcw25PEY#3?2wjV4x?pT;&nhqiFk!;QYwunqikvu*VgJnLIm?l-Vr}Q zI5q!zzGp)D=%ikV9wunaYM}kR;PG;e1`{*7I+WRUdQ#!}-4 zp#wAwqImYrT@+{*GkFH$uz~805gh1i1{sU_Vh8cL(e_cLJga4T^mgaw^@wg~%Z%(Kosj{K*K zxCleaW_kp+_ihC-*;)Ww<5B;lmg~V99txw4(rMwS+4k^`~YZ*a_v1QYzgX70*n2=3YOLr1zMhU4IR_DtFkBd>B zjmqTlk3@s`2e;WIJIQbDf6ycQ^G>3^$NOoJqt=@6MDj_*$`PqsBhtm?=X*SvdGwAe z2h+Amr5_X_u_UsLN+t`s56)#tZTLWG)nl^&#xlOMmB`Nl@Bb>Z`dD$RyWbuToxOma z@2aN69)G>D5ta!G>i8RA@nPXt;KV-9x@h+1?-A8?>+er!1g%VS zz6?0q?2Q}M|Jn0zr}`NMwUq;*d*;LdVUobl^+XeY)-g610B{)-1D*?cRHju0`C*jm zc!^f6f=ESSxK-o!JNdO_6t}-+KSgH!V>pYP13|+pVu;JXPtWB#y1KEK>9i8oJccG! z@V{PdTrbJ7^O|T_Kz(+{G&Cf?(Z?mX4%+|i%4hdW(ETEQrevbwtLCoWreLS*Gt##^ z4+t*og1KI<4yN)4li?k6ao%3yAsM@ywUEyzrkRi=asVZZC?A{F>C<(IBhUA=-3nXR z0l}8&QCs>D@kA3p4o!pXMhvS?7Gtte0}gmg%Tg%PlbDzo-1XIK^!1Lw^mDe7MRz47 z<;(~{z{8{duGxV1V3Xu$gM)UpX-7p#$!!aEo_&02DJfW(ywT@qf>R569aux5IWeh_ zrvJ?}B{eniBd<7v>iQ=|dG86`Jx5l9(rDVD@wC=i%5-M5d#{i0G8G9F&jN!BFtK&) zHU8n0`$qDR4S`{A4Z^-uR5yl@0$$GaQ8L%Pc^|8I>ZTHKacqGajs)OGS?^V?xtwG} zs!l#S&7a=~wiq}#Eal58Q+^g!%Mj({+nX zCM?`iVc3@aWq=W{U0KyH+G4TZZXz>;5uXjH_-8RTRWJy5#wI%v>OJ^VXUJeF9Z!tjPEZp&j2 zvIu|LTU`~6%!|{nX8Ou|d&(jt+(Jl1#75}y%_7os1^*v@?Dlzn;GjC?i6A|#`M=9O zt|6Hy86`bau+Ijkp`L*O3NV_=`Q)$_|3MZT+pAS+)K($J*pCzO&>OL_giE%&C4$(^ z2|;=<)r;MwsEuTPWI#i)Lf1ra`kvzL_E;u|JWJ};RVujT@A6+Ns;l+W=zt;Xx(!?! zGCuF`1&O1#<;dmb<%oh@8hS!S)A^JYj=eX3oyCl$@lYi31ml!7Oc;A)@GGeMd9YY! za7_r8t28PO+l%w%SY?x~o<7GCFssRF>aZu<+Sp8_^ZDd&Bt1O%6UypW<9vU+@~o;y zoHK!ou_W#{!XG7*kdoqJ;UETk_yt0WR4ypQuyu8Ksj;}2SavFH(F5*{!^2a%v?S#2 ze(rsPbAG<3Nk|kWQ!x0&xOu;AZvt9aRK)eqrL3u`+JHlx%GRC@u49ZP0v4dM97t}o0GMi$QRK!BOqd(yluH@A{C8Z$x)cY>#fZbfNy`Q3XEV7grD9GRZ*Q%%_ml9_)E9j7xf=5PVIk5Cu4XUYJWhSKg zvb?=6Td+T!lc=Ecp#Jkt2@xcz*-#`Psd+W^M$?$O$AXBa?`#{(L9zvF(7nZujfj6$ zF%=Jm$GOo`c7^EgHdd|FE5f?qJ~`!bNziih;m)vQO1LvdRcYyaO@0xPmEP$G&=uI2 ziDQnBsHi1^rOXN%8a#PB8Y+H)zb=x?S)*Iq0#26d^|LRt#RAsW#54(Gste(Gm$tRtsxR+ z{fdo6D{)NVa+_c)bowBMhWt^@FR+qpjZIKsw0=lTA~Y63n?@RT(0rM>5L&w~4jP!y z?EZ~@H8#Aq%jdK+t3%pylj`0po7?@Oc+5CV8XhF#6xVJN(aX92)y23Mn)1!%g!*;6 z!aX7^A|{>^V{CM^sivXR>kux5tbp?rycm>3k~87@ZGBTO!3>bq%+s)Uc*{zM5MCb1 zY|xb+VqrQ`X%@YN^Ag7NUv%|>X+cn^En#EjoEcN^n_(vRysFj;S-H6&i(7)6QE*=! z!&~$h&ZzqiWwbzYW10*q3Q`v&RF$vwFJUq5$PztQT&%;OUd8p%lE=pS8VE;kPDFkS zPZxnDHt;nw6AdWWM;qxEq>PiHrz@bTGdLae@oF}K!HyFW5*$BC$a~?lF_;qBzbCU-FM3uXt0w3Hwgw zt~3$2Q|gnlXy$>t$=BDHkc31)z)a2CTg}vTt#DX-SelY$R!3Kp5Jsh;gp;_W#b{FH9L9gXTUSme=>=2cx=OE7MhY6!(EFCPqWoEtt zkkO19y}}_T%|lzVR3hGmvcD1YL#Mnk{g(BHjg6Su&2UF_zx^|dUt$qt@2Zts{aZ1W z9URhnb#mx_9cghu{)AUGv%hLT76Q@dwlKc~a(kt-QGOWh*{#Q)Vlva&#s3VH0@Em* z;F($s)ngXiqyzs?*vizy!<%H~WgYEHw*=#86&O#QW5`lKTF|I;)S6c}PRo5*IHI$- zcx&*{*`Vt+-p`tV+m5ai=Cut8q&{+lG#XDJ(%y3iRi zt2pD3y9a0%MmumZgs7+HJ9YG{Ox8kv|IQ0Xd&tOu`|Ks{(#+!lUlkbl#`~Ehd<~3@ z*ooh~&04l~3oTkdGJ9y)Wr4=`-futLS@$Qt1-_%3T_#x5JO)`kl_TpywVY;0x6<%ER9(fyX%GvdM~Rh^w?m&3VVQprGK<3XMKX}(R$Ck0HT@-Tiw@jPGb-mg9U>3vJm}19krB24w)Kpu$zGev> z%5W3BG#>izsotZ`$d$q`o`sG){wM~}rttx2&rhFH}treG&lJe~vA~7k6IJKkN zfd)`eS9cAPuVONJwz;i-ynS-C;K|*(y%gi@#du02m%3O>umM+Du2=P6e2i(AODr!t z%%9*LGNST0zxw)PcebFWX;7vspln_9++KR`90wOZzPcsgulnTW)|OO+tc~ z{lTdBIw4{DaKTkZPL|840S{>J7z8!ojI*4Ol?Y0kjZ;lkRaMii0idC)3nsuoap?Vy zn+fbVTP!vxkOd4qah{iH_s2)zK2Dy64dM$leFF(sUT6nC=3#Gd?=VaZ=$@JB>&{*I zOcOHl>lc`nD0*U6>)sYVX>CqGeb4DDYijKGs-TXJPEX8g&YaphSMNZg|MT_XmoH!R zIH3OgSAt}X0nZ;duCA|3)H4rGB{KUhsYLGrME)02tE3QIy+aLGo~Q0x!>YfDxr{)l zL_iYK+1xDn_S(Vx_cDotLr1`^w~x=t^=S6&Y)Mg(jotWn>{qYQLMa4XT-L~WgC1rx zmX}wed|X)8TXK?Q@IEOpW{BD>FQJ<~HtFIlNhm2Pb!Pd>)B4_Gye)irfB&+o3IdYL zT4h%!YXKvrfsAgNi?`$lM{&nfEb_^+F$U$smY%)Td)e94;I#sOHZeACV|Ht-ub)Y& z>mw|hUmk>YK^SV2l#S4GQha`R zB+eXb@ud?NjwT`ZU+nF*oY|$lU#B{riDN3)gV1oBcene!DpAeJE3Vo-eC+H|FPXvp z2UkxnXmy)P0D(~ln9MeQVjL5{(9_fFI$GzmIk>uNNX?VSJv?kOX$MO>I7|d&%CfSu zfbNBby-e4Kr*@BptwD8{)#etzOF1GUBI20F;I)iP5{P)fjY*zcjHpOqxrTvw#{-Q3 zesX?eW8-%B&;6~?yse&VC{EkFc1pef?E^GTzRsX?#6oqgoM0aMvjg@%FC6d zrTcbsE5T^bqW1T^57+4h@&YPqm1)a|w>cAo?(J|rR${g`@=7~(tF9=5o8y&`qs1tY zvQjUl<22~-*WIt(N@P}FsX?%SUV4l*JbX_JsgRF-bwUqY6jSf3?W$W@ z`n%BW{?}9$Ti=*M60b}rBRLB;-^3*}U;gzpg~P-9Lx%X%Q?9<8oNf2&y$`;tyC4-c z_k#c_ViO*?l%SaSSGa&#S+g z`IlqZ>?TfI6WkXc7~E1-b+SDiJaQDMB6qQ xuzZi)b_}p_@4X)Z$eWCR{2$psnAP`;q?a?puJy>S2p&2Din3}lRZ`|*{|AY*Mmhih diff --git a/kured-ds.yaml b/kured-ds.yaml deleted file mode 100644 index b2df4db..0000000 --- a/kured-ds.yaml +++ /dev/null @@ -1,63 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: kured - namespace: kube-system ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: kured # Must match `--ds-name` - namespace: kube-system # Must match `--ds-namespace` -spec: - selector: - matchLabels: - name: kured - updateStrategy: - type: RollingUpdate - template: - metadata: - labels: - name: kured - spec: - serviceAccountName: kured - tolerations: - - key: node-role.kubernetes.io/master - effect: NoSchedule - hostPID: true # Facilitate entering the host mount namespace via init - restartPolicy: Always - containers: - - name: kured - image: docker.io/weaveworks/kured - # If you find yourself here wondering why there is no - # :latest tag on Docker Hub,see the FAQ in the README - imagePullPolicy: IfNotPresent - securityContext: - privileged: true # Give permission to nsenter /proc/1/ns/mnt - env: - # Pass in the name of the node on which this pod is scheduled - # for use with drain/uncordon operations and lock acquisition - - name: KURED_NODE_ID - valueFrom: - fieldRef: - fieldPath: spec.nodeName - command: - - /usr/bin/kured -# - --alert-filter-regexp=^RebootRequired$ -# - --blocking-pod-selector=runtime=long,cost=expensive -# - --blocking-pod-selector=name=temperamental -# - --blocking-pod-selector=... -# - --ds-name=kured -# - --ds-namespace=kube-system -# - --end-time=23:59:59 -# - --lock-annotation=weave.works/kured-node-lock -# - --period=1h -# - --prometheus-url=http://prometheus.monitoring.svc.cluster.local -# - --reboot-days=sun,mon,tue,wed,thu,fri,sat -# - --reboot-sentinel=/var/run/reboot-required -# - --slack-hook-url=https://hooks.slack.com/... -# - --slack-username=prod -# - --slack-channel=alerting -# - --start-time=0:00 -# - --time-zone=UTC diff --git a/kured-rbac.yaml b/kured-rbac.yaml deleted file mode 100644 index d425ce3..0000000 --- a/kured-rbac.yaml +++ /dev/null @@ -1,63 +0,0 @@ ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: kured -rules: -# Allow kured to read spec.unschedulable -# Allow kubectl to drain/uncordon -# -# NB: These permissions are tightly coupled to the bundled version of kubectl; the ones below -# match https://github.com/kubernetes/kubernetes/blob/v1.18.2/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain.go -# -- apiGroups: [""] - resources: ["nodes"] - verbs: ["get", "patch"] -- apiGroups: [""] - resources: ["pods"] - verbs: ["list","delete","get"] -- apiGroups: ["apps"] - resources: ["daemonsets"] - verbs: ["get"] -- apiGroups: [""] - resources: ["pods/eviction"] - verbs: ["create"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: kured -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: kured -subjects: -- kind: ServiceAccount - name: kured - namespace: kube-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - namespace: kube-system - name: kured -rules: -# Allow kured to lock/unlock itself -- apiGroups: ["apps"] - resources: ["daemonsets"] - resourceNames: ["kured"] - verbs: ["update"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - namespace: kube-system - name: kured -subjects: -- kind: ServiceAccount - namespace: kube-system - name: kured -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: kured diff --git a/pkg/alerts/prometheus.go b/pkg/alerts/prometheus.go deleted file mode 100644 index 9310729..0000000 --- a/pkg/alerts/prometheus.go +++ /dev/null @@ -1,52 +0,0 @@ -package alerts - -import ( - "context" - "fmt" - "regexp" - "sort" - "time" - - "github.com/prometheus/client_golang/api" - "github.com/prometheus/client_golang/api/prometheus/v1" - "github.com/prometheus/common/model" -) - -// Returns a list of names of active (e.g. pending or firing) alerts, filtered -// by the supplied regexp. -func PrometheusActiveAlerts(prometheusURL string, filter *regexp.Regexp) ([]string, error) { - client, err := api.NewClient(api.Config{Address: prometheusURL}) - if err != nil { - return nil, err - } - - queryAPI := v1.NewAPI(client) - - value, err := queryAPI.Query(context.Background(), "ALERTS", time.Now()) - if err != nil { - return nil, err - } - - if value.Type() == model.ValVector { - if vector, ok := value.(model.Vector); ok { - activeAlertSet := make(map[string]bool) - for _, sample := range vector { - if alertName, isAlert := sample.Metric[model.AlertNameLabel]; isAlert && sample.Value != 0 { - if filter == nil || !filter.MatchString(string(alertName)) { - activeAlertSet[string(alertName)] = true - } - } - } - - var activeAlerts []string - for activeAlert, _ := range activeAlertSet { - activeAlerts = append(activeAlerts, activeAlert) - } - sort.Sort(sort.StringSlice(activeAlerts)) - - return activeAlerts, nil - } - } - - return nil, fmt.Errorf("Unexpected value type: %v", value) -} diff --git a/pkg/daemonsetlock/daemonsetlock.go b/pkg/daemonsetlock/daemonsetlock.go deleted file mode 100644 index 9889b78..0000000 --- a/pkg/daemonsetlock/daemonsetlock.go +++ /dev/null @@ -1,123 +0,0 @@ -package daemonsetlock - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -type DaemonSetLock struct { - client *kubernetes.Clientset - nodeID string - namespace string - name string - annotation string -} - -type lockAnnotationValue struct { - NodeID string `json:"nodeID"` - Metadata interface{} `json:"metadata,omitempty"` -} - -func New(client *kubernetes.Clientset, nodeID, namespace, name, annotation string) *DaemonSetLock { - return &DaemonSetLock{client, nodeID, namespace, name, annotation} -} - -func (dsl *DaemonSetLock) Acquire(metadata interface{}) (acquired bool, owner string, err error) { - for { - ds, err := dsl.client.AppsV1().DaemonSets(dsl.namespace).Get(context.TODO(), dsl.name, metav1.GetOptions{}) - if err != nil { - return false, "", err - } - - valueString, exists := ds.ObjectMeta.Annotations[dsl.annotation] - if exists { - value := lockAnnotationValue{} - if err := json.Unmarshal([]byte(valueString), &value); err != nil { - return false, "", err - } - return value.NodeID == dsl.nodeID, value.NodeID, nil - } - - if ds.ObjectMeta.Annotations == nil { - ds.ObjectMeta.Annotations = make(map[string]string) - } - value := lockAnnotationValue{NodeID: dsl.nodeID, Metadata: metadata} - valueBytes, err := json.Marshal(&value) - if err != nil { - return false, "", err - } - ds.ObjectMeta.Annotations[dsl.annotation] = string(valueBytes) - - _, err = dsl.client.AppsV1().DaemonSets(dsl.namespace).Update(context.TODO(), ds, metav1.UpdateOptions{}) - if err != nil { - if se, ok := err.(*errors.StatusError); ok && se.ErrStatus.Reason == metav1.StatusReasonConflict { - // Something else updated the resource between us reading and writing - try again soon - time.Sleep(time.Second) - continue - } else { - return false, "", err - } - } - return true, dsl.nodeID, nil - } -} - -func (dsl *DaemonSetLock) Test(metadata interface{}) (holding bool, err error) { - ds, err := dsl.client.AppsV1().DaemonSets(dsl.namespace).Get(context.TODO(), dsl.name, metav1.GetOptions{}) - if err != nil { - return false, err - } - - valueString, exists := ds.ObjectMeta.Annotations[dsl.annotation] - if exists { - value := lockAnnotationValue{Metadata: metadata} - if err := json.Unmarshal([]byte(valueString), &value); err != nil { - return false, err - } - return value.NodeID == dsl.nodeID, nil - } - - return false, nil -} - -func (dsl *DaemonSetLock) Release() error { - for { - ds, err := dsl.client.AppsV1().DaemonSets(dsl.namespace).Get(context.TODO(), dsl.name, metav1.GetOptions{}) - if err != nil { - return err - } - - valueString, exists := ds.ObjectMeta.Annotations[dsl.annotation] - if exists { - value := lockAnnotationValue{} - if err := json.Unmarshal([]byte(valueString), &value); err != nil { - return err - } - if value.NodeID != dsl.nodeID { - return fmt.Errorf("Not lock holder: %v", value.NodeID) - } - } else { - return fmt.Errorf("Lock not held") - } - - delete(ds.ObjectMeta.Annotations, dsl.annotation) - - _, err = dsl.client.AppsV1().DaemonSets(dsl.namespace).Update(context.TODO(), ds, metav1.UpdateOptions{}) - if err != nil { - if se, ok := err.(*errors.StatusError); ok && se.ErrStatus.Reason == metav1.StatusReasonConflict { - // Something else updated the resource between us reading and writing - try again soon - time.Sleep(time.Second) - continue - } else { - return err - } - } - return nil - } -} diff --git a/pkg/delaytick/delaytick.go b/pkg/delaytick/delaytick.go deleted file mode 100644 index 4673bb9..0000000 --- a/pkg/delaytick/delaytick.go +++ /dev/null @@ -1,22 +0,0 @@ -package delaytick - -import ( - "math/rand" - "time" -) - -// Tick regularly after an initial delay randomly distributed between d/2 and d + d/2 -func New(s rand.Source, d time.Duration) <-chan time.Time { - c := make(chan time.Time) - - go func() { - random := rand.New(s) - time.Sleep(time.Duration(float64(d)/2 + float64(d)*random.Float64())) - c <- time.Now() - for t := range time.Tick(d) { - c <- t - } - }() - - return c -} diff --git a/pkg/notifications/slack/slack.go b/pkg/notifications/slack/slack.go deleted file mode 100644 index 30ac4c5..0000000 --- a/pkg/notifications/slack/slack.go +++ /dev/null @@ -1,52 +0,0 @@ -package slack - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "time" -) - -var ( - httpClient = &http.Client{Timeout: 5 * time.Second} -) - -type body struct { - Text string `json:"text,omitempty"` - Username string `json:"username,omitempty"` - Channel string `json:"channel,omitempty"` -} - -func notify(hookURL, username, channel, message string) error { - msg := body{ - Text: message, - Username: username, - Channel: channel, - } - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(&msg); err != nil { - return err - } - - resp, err := httpClient.Post(hookURL, "application/json", &buf) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf(resp.Status) - } - - return nil -} - -func NotifyDrain(hookURL, username, channel, nodeID string) error { - return notify(hookURL, username, channel, fmt.Sprintf("Draining node %s", nodeID)) -} - -func NotifyReboot(hookURL, username, channel, nodeID string) error { - return notify(hookURL, username, channel, fmt.Sprintf("Rebooting node %s", nodeID)) -} diff --git a/pkg/timewindow/days.go b/pkg/timewindow/days.go deleted file mode 100644 index 2635c84..0000000 --- a/pkg/timewindow/days.go +++ /dev/null @@ -1,91 +0,0 @@ -package timewindow - -import ( - "fmt" - "strconv" - "strings" - "time" -) - -var EveryDay = []string{"su", "mo", "tu", "we", "th", "fr", "sa"} - -// dayStrings maps day strings to time.Weekdays -var dayStrings = map[string]time.Weekday{ - "su": time.Sunday, - "sun": time.Sunday, - "sunday": time.Sunday, - "mo": time.Monday, - "mon": time.Monday, - "monday": time.Monday, - "tu": time.Tuesday, - "tue": time.Tuesday, - "tuesday": time.Tuesday, - "we": time.Wednesday, - "wed": time.Wednesday, - "wednesday": time.Wednesday, - "th": time.Thursday, - "thu": time.Thursday, - "thursday": time.Thursday, - "fr": time.Friday, - "fri": time.Friday, - "friday": time.Friday, - "sa": time.Saturday, - "sat": time.Saturday, - "saturday": time.Saturday, -} - -type weekdays uint32 - -// parseWeekdays creates a set of weekdays from a string slice -func parseWeekdays(days []string) (weekdays, error) { - var result uint32 - for _, day := range days { - if len(day) == 0 { - continue - } - - weekday, err := parseWeekday(day) - if err != nil { - return weekdays(0), err - } - - result |= 1 << uint32(weekday) - } - - return weekdays(result), nil -} - -// Contains returns true if the specified weekday is a member of this set. -func (w weekdays) Contains(day time.Weekday) bool { - return uint32(w)&(1<= 0 && n < 7 { - return time.Weekday(n), nil - } else { - return time.Sunday, fmt.Errorf("Invalid weekday, number out of range: %s", day) - } - } - - if weekday, ok := dayStrings[strings.ToLower(day)]; ok { - return weekday, nil - } else { - return time.Sunday, fmt.Errorf("Invalid weekday: %s", day) - } -} diff --git a/pkg/timewindow/days_test.go b/pkg/timewindow/days_test.go deleted file mode 100644 index 8f5f00a..0000000 --- a/pkg/timewindow/days_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package timewindow - -import ( - "strings" - "testing" -) - -func TestParseWeekdays(t *testing.T) { - tests := []struct { - input string - result string - }{ - {"0,4", "Sun---------Thu------"}, - {"su,mo,tu", "SunMonTue------------"}, - {"sunday,tu,thu", "Sun---Tue---Thu------"}, - {"THURSDAY", "------------Thu------"}, - {"we,WED,WeDnEsDaY", "---------Wed---------"}, - {"", "---------------------"}, - {",,,", "---------------------"}, - } - - for _, tst := range tests { - res, err := parseWeekdays(strings.Split(tst.input, ",")) - if err != nil { - t.Errorf("Received error for input %s: %v", tst.input, err) - } else if res.String() != tst.result { - t.Errorf("Test %s: Expected %s got %s", tst.input, tst.result, res.String()) - } - } -} - -func TestParseWeekdaysErrors(t *testing.T) { - tests := []string{ - "15", - "-8", - "8", - "mon,tue,wed,fridayyyy", - } - - for _, tst := range tests { - _, err := parseWeekdays(strings.Split(tst, ",")) - if err == nil { - t.Errorf("Expected to receive error for input %s", tst) - } - } -} diff --git a/pkg/timewindow/timewindow.go b/pkg/timewindow/timewindow.go deleted file mode 100644 index 6ac1a0e..0000000 --- a/pkg/timewindow/timewindow.go +++ /dev/null @@ -1,68 +0,0 @@ -package timewindow - -import ( - "fmt" - "time" -) - -// TimeWindow specifies a schedule of days and times. -type TimeWindow struct { - days weekdays - location *time.Location - startTime time.Time - endTime time.Time -} - -// New creates a TimeWindow instance based on string inputs specifying a schedule. -func New(days []string, startTime, endTime, location string) (*TimeWindow, error) { - tw := &TimeWindow{} - - var err error - if tw.days, err = parseWeekdays(days); err != nil { - return nil, err - } - - if tw.location, err = time.LoadLocation(location); err != nil { - return nil, err - } - - if tw.startTime, err = parseTime(startTime, tw.location); err != nil { - return nil, err - } - - if tw.endTime, err = parseTime(endTime, tw.location); err != nil { - return nil, err - } - - return tw, nil -} - -// Contains determines whether the specified time is within this time window. -func (tw *TimeWindow) Contains(t time.Time) bool { - loctime := t.In(tw.location) - if !tw.days.Contains(loctime.Weekday()) { - return false - } - - start := time.Date(loctime.Year(), loctime.Month(), loctime.Day(), tw.startTime.Hour(), tw.startTime.Minute(), tw.startTime.Second(), 0, tw.location) - end := time.Date(loctime.Year(), loctime.Month(), loctime.Day(), tw.endTime.Hour(), tw.endTime.Minute(), tw.endTime.Second(), 1e9-1, tw.location) - - return (loctime.After(start) || loctime.Equal(start)) && (loctime.Before(end) || loctime.Equal(end)) -} - -// String returns a string representation of this time window. -func (tw *TimeWindow) String() string { - return fmt.Sprintf("%s between %02d:%02d and %02d:%02d %s", tw.days.String(), tw.startTime.Hour(), tw.startTime.Minute(), tw.endTime.Hour(), tw.endTime.Minute(), tw.location.String()) -} - -// parseTime tries to parse a time with several formats. -func parseTime(s string, loc *time.Location) (time.Time, error) { - fmts := []string{"15:04", "15:04:05", "03:04pm", "15", "03pm", "3pm"} - for _, f := range fmts { - if t, err := time.ParseInLocation(f, s, loc); err == nil { - return t, nil - } - } - - return time.Now(), fmt.Errorf("Invalid time format: %s", s) -} diff --git a/pkg/timewindow/timewindow_test.go b/pkg/timewindow/timewindow_test.go deleted file mode 100644 index a629a45..0000000 --- a/pkg/timewindow/timewindow_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package timewindow - -import ( - "strings" - "testing" - "time" -) - -func TestTimeWindows(t *testing.T) { - type testcase struct { - time string - result bool - } - - tests := []struct { - days string - start string - end string - loc string - cases []testcase - }{ - {"mon,tue,wed,thu,fri", "9am", "5pm", "America/Los_Angeles", []testcase{ - {"2019/04/04 00:49 PDT", false}, - {"2019/04/05 08:59 PDT", false}, - {"2019/04/05 9:01 PDT", true}, - {"2019/03/31 10:00 PDT", false}, - {"2019/04/04 12:00 PDT", true}, - {"2019/04/04 11:59 UTC", false}, - }}, - {"mon,we,fri", "10:01", "11:30am", "America/Los_Angeles", []testcase{ - {"2019/04/05 10:30 PDT", true}, - {"2019/04/06 10:30 PDT", false}, - {"2019/04/07 10:30 PDT", false}, - {"2019/04/08 10:30 PDT", true}, - {"2019/04/09 10:30 PDT", false}, - {"2019/04/10 10:30 PDT", true}, - {"2019/04/11 10:30 PDT", false}, - }}, - {"mo,tu,we,th,fr", "00:00", "23:59:59", "UTC", []testcase{ - {"2019/04/18 00:00 UTC", true}, - {"2019/04/18 23:59 UTC", true}, - }}, - } - - for i, tst := range tests { - tw, err := New(strings.Split(tst.days, ","), tst.start, tst.end, tst.loc) - if err != nil { - t.Errorf("Test [%d] failed to create TimeWindow: %v", i, err) - } - - for _, cas := range tst.cases { - tm, err := time.ParseInLocation("2006/01/02 15:04 MST", cas.time, tw.location) - if err != nil { - t.Errorf("Failed to parse time \"%s\": %v", cas.time, err) - } else if cas.result != tw.Contains(tm) { - t.Errorf("(%s) contains (%s) didn't match expected result of %v", tw.String(), cas.time, cas.result) - } - } - } -}