diff --git a/.gitignore b/.gitignore index 6b612cb..8e3620c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /k-rail +/evicter vendor diff --git a/Dockerfile b/Dockerfile index 1a69d35..e5e859f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG GO_VERSION=1.13 FROM golang:${GO_VERSION}-buster AS builder WORKDIR /build COPY ./ /build/ -RUN make test +RUN make clean test RUN make build # Production image build stage @@ -11,5 +11,6 @@ FROM scratch EXPOSE 8443/tcp COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /build/k-rail /k-rail +COPY --from=builder /build/evicter /evicter USER 65534 ENTRYPOINT ["/k-rail", "-config", "/config/config.yml"] diff --git a/Makefile b/Makefile index d5d5857..dc9f74b 100644 --- a/Makefile +++ b/Makefile @@ -9,13 +9,27 @@ # 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. +.PHONY: all build test image clean -ensure: - dep ensure +LDFLAGS = -extldflags=-static -s -w +BUILD_FLAGS = -mod=readonly -ldflags '$(LDFLAGS)' -trimpath +BUILD_VERSION ?= manual +IMAGE_NAME = "cruise/k-rail:${BUILD_VERSION}" + +all: dist + +dist: image + +clean: + rm -f evicter k-rail + go mod verify build: - GO111MODULE=on CGO_ENABLED=0 go build -o k-rail cmd/main.go + GO111MODULE=on CGO_ENABLED=0 go build ${BUILD_FLAGS} -o k-rail cmd/k-rail/main.go + GO111MODULE=on CGO_ENABLED=0 go build ${BUILD_FLAGS} -o evicter cmd/evicter/*.go test: - GO111MODULE=on CGO_ENABLED=1 go test -race -cover $(shell go list ./... | grep -v /vendor/) - \ No newline at end of file + GO111MODULE=on CGO_ENABLED=1 go test -mod=readonly -race ./... + +image: build + docker build --pull -t $(IMAGE_NAME) . \ No newline at end of file diff --git a/cmd/evicter/README.md b/cmd/evicter/README.md new file mode 100644 index 0000000..30d6a76 --- /dev/null +++ b/cmd/evicter/README.md @@ -0,0 +1,6 @@ +# Evict tainted pods after period +Operator that runs within k8s to find and evict `tainted` pods. + +* `k-rail/tainted-timestamp` store the unix timestamp when the root event happend +* `k-rail/tainted-prevent-eviction` is a break-glass annotation to prevent automated eviction +* `k-rail/reason` intended for humans diff --git a/cmd/evicter/controller.go b/cmd/evicter/controller.go new file mode 100644 index 0000000..9898a21 --- /dev/null +++ b/cmd/evicter/controller.go @@ -0,0 +1,188 @@ +package main + +import ( + "fmt" + "strconv" + "time" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog" +) + +type podProvisioner interface { + Evict(pod *v1.Pod, reason, msg string) error +} + +type Controller struct { + podStore cache.Store + queue workqueue.RateLimitingInterface + informer cache.Controller + podProvisioner podProvisioner + incubationPeriodSeconds time.Duration + started time.Time +} + +func NewController(queue workqueue.RateLimitingInterface, indexer cache.Store, informer cache.Controller, podProvisioner podProvisioner, incubationPeriodSeconds int64) *Controller { + return &Controller{ + informer: informer, + podStore: indexer, + queue: queue, + podProvisioner: podProvisioner, + incubationPeriodSeconds: time.Duration(incubationPeriodSeconds) * time.Second, + } +} + +func (c *Controller) processNextItem() bool { + key, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(key) + + err := c.evictPod(key.(string)) + c.handleErr(err, key) + return true +} + +const ( + // annotationPreventEviction is a break-glass annotation to prevent automated eviction + annotationPreventEviction = "k-rail/tainted-prevent-eviction" + // annotationTimestamp stores the unix timestamp when the root event happened + annotationTimestamp = "k-rail/tainted-timestamp" + // annotationReason is used to define any additional reason in a human readable form + annotationReason = "k-rail/tainted-reason" +) + +const defaultEvictionReason = "Tainted" +const noEvictionNote = "Pod was marked as tainted" + +// evictPod is the business logic of the controller. it checks the the eviction rules and conditions before calling the pod provisioner. +func (c *Controller) evictPod(key string) error { + obj, exists, err := c.podStore.GetByKey(key) + switch { + case err != nil: + return err + case !exists: + return nil + } + pod, ok := obj.(*v1.Pod) + if !ok { + return fmt.Errorf("unsupported type: %T", obj) + } + if !canEvict(pod, c.incubationPeriodSeconds) { + return nil + } + + msg, ok := pod.Annotations[annotationReason] + if !ok || msg == "" { + msg = noEvictionNote + } + return c.podProvisioner.Evict(pod, defaultEvictionReason, msg) +} + +func canEvict(pod *v1.Pod, incubationPeriod time.Duration) bool { + if pod == nil { + return false + } + switch pod.Annotations[annotationPreventEviction] { + case "yes", "true", "1", "YES", "TRUE", "Yes", "True": + return false + } + + val, ok := pod.Annotations[annotationTimestamp] + if ok { + i, err := strconv.ParseInt(val, 10, 64) + if err != nil { + // todo: log + return true + } + timestamp := time.Unix(i, 0) + if time.Since(timestamp) < incubationPeriod { + return false + } + } + return true +} + +const maxWorkerRetries = 5 + +// handleErr checks if an error happened and makes sure we will retry later. +func (c *Controller) handleErr(err error, key interface{}) { + if err == nil { + // Forget about the #AddRateLimited history of the key on every successful synchronization. + // This ensures that future processing of updates for this key is not delayed because of + // an outdated error history. + c.queue.Forget(key) + return + } + + // This controller retries 5 times if something goes wrong. After that, it stops trying. + if c.queue.NumRequeues(key) < maxWorkerRetries { + klog.Infof("Error syncing pod %v: %v", key, err) + + // Re-enqueue the key rate limited. Based on the rate limiter on the + // queue and the re-enqueue history, the key will be processed later again. + c.queue.AddRateLimited(key) + return + } + + c.queue.Forget(key) + // Report to an external entity that, even after several retries, we could not successfully process this key + runtime.HandleError(err) + klog.Infof("Dropping pod %q out of the queue: %v", key, err) +} + +const reconciliationTick = 30 * time.Second +const startupGracePeriod = 90 * time.Second + +func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) { + defer runtime.HandleCrash() + + // Let the workers stop when we are done + defer c.queue.ShutDown() + klog.Info("Starting Pod controller") + + go c.informer.Run(stopCh) + + // Wait for all involved caches to be synced, before processing items from the queue is started + if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) { + runtime.HandleError(fmt.Errorf("timed out waiting for caches to sync")) + return + } + + for i := 0; i < threadiness; i++ { + go wait.Until(c.runWorker, time.Second, stopCh) + } + + wait.Until(func() { + if time.Since(c.started) < startupGracePeriod { + return + } + if err := c.doReconciliation(); err != nil { + klog.Errorf("Reconciliation failed: %s", err) + } + }, reconciliationTick, stopCh) + + klog.Info("Stopping Pod controller") +} + +func (c *Controller) runWorker() { + for c.processNextItem() { + } +} + +func (c *Controller) doReconciliation() error { + klog.Info("Reconciliation started") + for _, key := range c.podStore.ListKeys() { + if err := c.evictPod(key); err != nil { + return errors.Wrapf(err, "pod %q", key) + } + } + klog.Info("Reconciliation completed") + return nil +} diff --git a/cmd/evicter/controller_test.go b/cmd/evicter/controller_test.go new file mode 100644 index 0000000..deaceb1 --- /dev/null +++ b/cmd/evicter/controller_test.go @@ -0,0 +1,167 @@ +package main + +import ( + "reflect" + "strconv" + "testing" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +func TestCanEvict(t *testing.T) { + now := int(time.Now().Unix()) + specs := map[string]struct { + srcAnn map[string]string + expResult bool + }{ + "with timestamp after incubation period": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + }, + expResult: true, + }, + "with timestamp in incubation period": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now), + "k-rail/tainted-reason": "test", + }, + expResult: false, + }, + "without timestamp annotation": { + srcAnn: map[string]string{ + "k-rail/tainted-reason": "test", + }, + expResult: true, + }, + "with timestamp containing non timestamp string": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": "", + "k-rail/tainted-reason": "test", + }, + expResult: true, + }, + "with preventEviction annotation": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "true", + }, + expResult: false, + }, + "with preventEviction annotation - uppercase": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "TRUE", + }, + expResult: false, + }, + "with preventEviction annotation - yes": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "yes", + }, + expResult: false, + }, + "with preventEviction annotation - non bool": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "", + }, + expResult: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + pod := v1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: spec.srcAnn}} + if got := canEvict(&pod, time.Second); spec.expResult != got { + t.Errorf("expected %v but got %v", spec.expResult, got) + } + }) + } +} + +func TestEvictPod(t *testing.T) { + now := int(time.Now().Unix()) + + specs := map[string]struct { + srcAnn map[string]string + expReason string + expMsg string + expNoEviction bool + }{ + "evicted with custom reason": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + }, + expReason: "Tainted", + expMsg: "test", + }, + "evicted with default reason": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + }, + expReason: "Tainted", + expMsg: noEvictionNote, + }, + "not evicted with annotation": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-prevent-eviction": "yes", + }, + expNoEviction: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "myPod", Annotations: spec.srcAnn}} + store.Add(pod) + prov := &recordingPodProvisioner{} + c := NewController(nil, store, nil, prov, 1) + // when + err := c.evictPod("myPod") + // then + if err != nil { + t.Fatalf("unexpected error: %+v", err) + } + if spec.expNoEviction { + if prov.evictedPods != nil { + t.Fatalf("expected no call but got %v", prov.evictedPods) + } + return + } + // there should be 1 call + if exp, got := []*v1.Pod{pod}, prov.evictedPods; !reflect.DeepEqual(exp, got) { + t.Errorf("expected %v but got %v", exp, got) + } + if exp, got := []string{spec.expReason}, prov.reasons; !reflect.DeepEqual(exp, got) { + t.Errorf("expected %v but got %v", exp, got) + } + if exp, got := []string{spec.expMsg}, prov.msgs; !reflect.DeepEqual(exp, got) { + t.Errorf("expected %v but got %v", exp, got) + } + }) + } +} + +type recordingPodProvisioner struct { + evictedPods []*v1.Pod + reasons []string + msgs []string + result error +} + +func (r *recordingPodProvisioner) Evict(pod *v1.Pod, reason, msg string) error { + r.evictedPods = append(r.evictedPods, pod) + r.reasons = append(r.reasons, reason) + r.msgs = append(r.msgs, msg) + return r.result +} diff --git a/cmd/evicter/main.go b/cmd/evicter/main.go new file mode 100644 index 0000000..b3f370e --- /dev/null +++ b/cmd/evicter/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "context" + "flag" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/cruise-automation/k-rail/resource" + "github.com/golang/glog" + "github.com/google/uuid" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + policy "k8s.io/api/policy/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/kubernetes/typed/policy/v1beta1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/leaderelection" + "k8s.io/client-go/tools/leaderelection/resourcelock" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog" +) + +func main() { + var ( + kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file: `/.kube/config`") + master = flag.String("master", "", "master url") + labelSelector = flag.String("label-selector", "tainted=true", "label selector to discover tainted pods") + terminationGracePeriodSeconds = flag.Int64("termination-grace-period", 30, "pod termination grace period in seconds") + taintedIncubationPeriodSeconds = flag.Int64("incubation-period", 24*60*60, "time in seconds a tainted pod can run before eviction") + instanceID = flag.String("instance", uuid.New().String(), "the unique holder identity. used for leader lock") + leaseLockName = flag.String("lease-lock-name", "k-rail-evicter-lock", "the lease lock resource name") + leaseLockNamespace = flag.String("lease-lock-namespace", "k-rail", "the lease lock resource namespace") + probeServerAddress = flag.String("probe-listen-address", ":8080", "server address for healthz/readiness server") + ) + flag.Parse() + flag.Set("logtostderr", "true") // glog: no disk log + + defer func() { + if err := recover(); err != nil { + klog.Fatal(err) + } + }() + + config, err := clientcmd.BuildConfigFromFlags(*master, *kubeconfig) + if err != nil { + klog.Fatal(err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + klog.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + watchSigTerm(cancel) + + probeServer := newProbeServer(*probeServerAddress) + go func() { + klog.Fatal(probeServer.ListenAndServe()) + }() + + br := record.NewBroadcaster() + br.StartRecordingToSink(&corev1.EventSinkImpl{Interface: clientset.CoreV1().Events(metav1.NamespaceAll)}) + br.StartLogging(glog.Infof) + defer br.Shutdown() + eventRecorder := br.NewRecorder(scheme.Scheme, v1.EventSource{Component: "k-rail-evicter"}) + + lock := &resourcelock.LeaseLock{ + LeaseMeta: metav1.ObjectMeta{ + Name: *leaseLockName, + Namespace: *leaseLockNamespace, + }, + Client: clientset.CoordinationV1(), + LockConfig: resourcelock.ResourceLockConfig{ + Identity: *instanceID, + EventRecorder: eventRecorder, + }, + } + + // start the leader election code loop + leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ + Lock: lock, + ReleaseOnCancel: true, + LeaseDuration: 60 * time.Second, + RenewDeadline: 15 * time.Second, + RetryPeriod: 5 * time.Second, + Callbacks: leaderelection.LeaderCallbacks{ + OnStartedLeading: func(ctx context.Context) { + klog.Infof("Starting leader: %s", *instanceID) + podListWatcher := cache.NewFilteredListWatchFromClient(clientset.CoreV1().RESTClient(), "pods", metav1.NamespaceAll, + func(options *metav1.ListOptions) { + options.LabelSelector = *labelSelector + }) + + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + + // Bind the workqueue to a cache with the help of an informer. This way we make sure that + // whenever the cache is updated, the pod key is added to the workqueue. + // Note that when we finally process the item from the workqueue, we might see a newer version + // of the Pod than the version which was responsible for triggering the update. + indexer, informer := cache.NewIndexerInformer(podListWatcher, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + if key, err := cache.MetaNamespaceKeyFunc(obj); err == nil { + queue.Add(key) + } + }, + UpdateFunc: func(old interface{}, new interface{}) { + if key, err := cache.MetaNamespaceKeyFunc(new); err == nil { + queue.Add(key) + } + }, + DeleteFunc: func(obj interface{}) { + // IndexerInformer uses a delta queue, therefore for deletes we have to use this key function. + if key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err == nil { + queue.Add(key) + } + }, + }, cache.Indexers{}) + + evicter := newPodEvicter(clientset.PolicyV1beta1(), eventRecorder, *terminationGracePeriodSeconds) + controller := NewController(queue, indexer, informer, evicter, *taintedIncubationPeriodSeconds) + controller.Run(1, ctx.Done()) + }, + OnStoppedLeading: func() { + // we can do cleanup here + cancel() + ctx, _ := context.WithTimeout(ctx, 100*time.Millisecond) + _ = probeServer.Shutdown(ctx) + klog.Infof("Leader lost: %s", *instanceID) + os.Exit(0) + }, + OnNewLeader: func(identity string) { + if identity == *instanceID { + return + } + klog.Infof("New leader elected: %s", identity) + }, + }, + }) +} + +func newProbeServer(listenAddr string) http.Server { + okHandler := func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + } + http.HandleFunc("/healthz", okHandler) + http.HandleFunc("/readyness", okHandler) + return http.Server{Addr: listenAddr, Handler: nil} +} + +func watchSigTerm(cancel context.CancelFunc) { + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + go func() { + <-ch + klog.Info("Received termination, signaling shutdown") + cancel() + }() +} + +type podEvicter struct { + client v1beta1.PolicyV1beta1Interface + eventRecorder record.EventRecorder + defaultDeleteOptions *metav1.DeleteOptions +} + +func newPodEvicter(client v1beta1.PolicyV1beta1Interface, recorder record.EventRecorder, gracePeriodSeconds int64) *podEvicter { + return &podEvicter{ + client: client, + eventRecorder: recorder, + defaultDeleteOptions: &metav1.DeleteOptions{GracePeriodSeconds: &gracePeriodSeconds}, + } +} + +// Evict calls the k8s api to evict the given pod. Reason and notes are stored with the audit event. +func (p *podEvicter) Evict(pod *v1.Pod, reason, msg string) error { + err := p.client.Evictions(pod.Namespace).Evict(newEviction(pod, p.defaultDeleteOptions)) + klog.Infof("Evicted pod %q (UID: %s)", resource.GetResourceName(pod.ObjectMeta), pod.UID) + if err != nil { + return errors.Wrap(err, "eviction") + } + p.eventRecorder.Eventf(pod, v1.EventTypeNormal, reason, msg) + return nil +} + +func newEviction(pod *v1.Pod, deleteOption *metav1.DeleteOptions) *policy.Eviction { + return &policy.Eviction{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "Policy/v1beta1", + Kind: "Eviction", + }, + ObjectMeta: pod.ObjectMeta, + DeleteOptions: deleteOption, + } +} diff --git a/cmd/main.go b/cmd/k-rail/main.go similarity index 100% rename from cmd/main.go rename to cmd/k-rail/main.go diff --git a/go.mod b/go.mod index 9629bcf..0cc3cf8 100644 --- a/go.mod +++ b/go.mod @@ -4,18 +4,20 @@ go 1.12 require ( github.com/gobwas/glob v0.2.3 + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/protobuf v1.3.4 // indirect + github.com/google/uuid v1.1.1 github.com/gorilla/mux v1.7.0 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 + github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.3.0 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect k8s.io/api v0.17.2 k8s.io/apimachinery v0.17.2 k8s.io/client-go v0.17.2 //v11.0.0+incompatible + k8s.io/klog v1.0.0 k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab sigs.k8s.io/yaml v1.1.0 ) - -replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0 diff --git a/go.sum b/go.sum index b7e4250..16f540a 100644 --- a/go.sum +++ b/go.sum @@ -21,7 +21,9 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/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 h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 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= @@ -33,7 +35,9 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= 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= @@ -52,6 +56,7 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 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 h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= @@ -61,8 +66,11 @@ github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 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 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= @@ -90,12 +98,16 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m 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.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/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 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -108,7 +120,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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 v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= @@ -116,7 +127,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P 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 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -153,7 +163,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/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= @@ -170,24 +179,23 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 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 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= 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/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 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -204,6 +212,7 @@ k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUc 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-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab h1:I3f2hcBrepGRXI1z4sukzAb8w1R4eqbsHrAsx06LGYM=